@kurokeita/add-skill 2.5.0 → 2.6.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/dist/bin/cli.js +1 -1
- package/dist/skills/git-commit/SKILL.md +1 -1
- package/dist/skills/git-commit/hooks/commit-gate.py +1 -1
- package/dist/skills/statusline-setup/SKILL.md +1 -1
- package/dist/skills/statusline-setup/mod/hooks/register.tsx +5 -2
- package/dist/skills/statusline-setup/mod/tests/work.test.ts +56 -1
- package/package.json +1 -1
package/dist/bin/cli.js
CHANGED
|
@@ -75,7 +75,7 @@ Print exactly: **"Commit as-is, edit the message, or skip?"**
|
|
|
75
75
|
|
|
76
76
|
Steps 2 and 3 are your final chat message, using the exact headings and bullet labels above. Put the commit message in a code fence directly under its heading. The fence is the message's only boundary, so any note after the fence stays out of the approved message. End the turn there, with no tool call after the proposal. The `~/.claude/hooks/commit-gate.py` Stop hook checks the message and either lists what is missing or records the proposal and sends you back. Then ask through AskUserQuestion with that text as the question and "Commit as-is" as the first option. The preview can hold just the commit message. The same hook denies a commit question with no recorded proposal, and denies `git commit` until the user picks "Commit as-is". Each approval covers one commit. Commit with the approved message verbatim in a quoted heredoc (`-m "$(cat <<'EOF' ... EOF)"`), with no attribution trailers: the hook denies a changed message, `-F`, the editor, `--amend` without `-m`, and any `Co-Authored-By` or "Generated with Claude Code" line.
|
|
77
77
|
|
|
78
|
-
**When the commit-gate hook is disabled**, the Stop hook never sends you back, so ending the turn would leave the user with no question. To turn the hook off for one session, the user starts Claude Code with `COMMIT_GATE_OFF=1` set. Before emitting the proposal, run `printenv COMMIT_GATE_OFF; grep -s 'commit-gate.py\|"disableAllHooks": true' ~/.claude/settings.json .claude/settings.json .claude/settings.local.json`. The hook is disabled when `COMMIT_GATE_OFF` prints
|
|
78
|
+
**When the commit-gate hook is disabled**, the Stop hook never sends you back, so ending the turn would leave the user with no question. To turn the hook off for one session, the user starts Claude Code with `COMMIT_GATE_OFF=1` or `COMMIT_GATE_OFF=true` set. Before emitting the proposal, run `printenv COMMIT_GATE_OFF; grep -s 'commit-gate.py\|"disableAllHooks": true' ~/.claude/settings.json .claude/settings.json .claude/settings.local.json`. The hook is disabled when `COMMIT_GATE_OFF` prints `1` or `true` (any other value leaves it on), when no line names `commit-gate.py`, or when a line sets `"disableAllHooks": true`. In that case, emit the proposal, then call AskUserQuestion right away in the same turn with the same question, options, and preview. Every other rule still holds: one approval per commit, the approved message verbatim in a quoted heredoc, no attribution trailers. Nothing enforces them, so apply them yourself.
|
|
79
79
|
|
|
80
80
|
**Standing approval.** The user can tell you in this session to commit without asking, for example so they can step away. That approval applies only while the hook is disabled. For each commit, still emit the proposal so the user can review it later, then skip AskUserQuestion and commit with that message. Standing approval covers new commits only, made with `git commit -m`. Every other gated command (`--amend`, force push, `--tags`, `reset --hard`, `rebase`, `gh pr create`, `gh pr merge`) still needs its own approval. Never infer standing approval from a plan approval or an earlier "go ahead".
|
|
81
81
|
|
|
@@ -199,7 +199,7 @@ def deny(reason):
|
|
|
199
199
|
|
|
200
200
|
|
|
201
201
|
def main():
|
|
202
|
-
if os.environ.get("COMMIT_GATE_OFF"):
|
|
202
|
+
if os.environ.get("COMMIT_GATE_OFF") in ("1", "true"):
|
|
203
203
|
return
|
|
204
204
|
payload = json.load(sys.stdin)
|
|
205
205
|
path = os.path.join(STATE_DIR, f"{payload.get('session_id', 'unknown')}.json")
|
|
@@ -1256,7 +1256,7 @@ The work box shows a few rows. The work pane is its scrollable companion: a Clau
|
|
|
1256
1256
|
|
|
1257
1257
|
What it shows, in a side pane:
|
|
1258
1258
|
|
|
1259
|
-
- **Summary and Steps** of the session, written by a Haiku call. It runs when the pane first opens and on **Refresh
|
|
1259
|
+
- **Summary and Steps** of the session, written by a Haiku call. It runs when the pane first opens, at the end of each turn while the pane is open, and on **Refresh**.
|
|
1260
1260
|
- **Pull requests**: the current branch's, those of branches this session committed to, and any PR of this repository linked in the conversation. Each shows its state, review, checks and comment count.
|
|
1261
1261
|
- **Work items**: the PRs' linked issues or work items or, for a branch without a PR, the same branch refs the work box uses.
|
|
1262
1262
|
|
|
@@ -232,11 +232,12 @@ export const register: Register = (on) => {
|
|
|
232
232
|
on("turn.complete", async ($, e, next) => {
|
|
233
233
|
const result = await next(e)
|
|
234
234
|
if (e.agentId) return result
|
|
235
|
-
// A main-conversation turn is when the session pushes branches and opens PRs
|
|
235
|
+
// A main-conversation turn is when the session pushes branches and opens PRs,
|
|
236
|
+
// and moves its goal and steps on.
|
|
236
237
|
void (async () => {
|
|
237
238
|
await noteBranch($)
|
|
238
239
|
if (isAutoOpenWanted && !isAutoOpened) return autoOpen($)
|
|
239
|
-
if (await isPaneOpen($)) await refreshWork($)
|
|
240
|
+
if (await isPaneOpen($)) await Promise.all([refreshWork($), summarize($)])
|
|
240
241
|
})()
|
|
241
242
|
|
|
242
243
|
return result
|
|
@@ -249,6 +250,8 @@ export const register: Register = (on) => {
|
|
|
249
250
|
})
|
|
250
251
|
|
|
251
252
|
on("command.run", { command: "work" }, async ($) => {
|
|
253
|
+
// Opened by hand: auto-open has nothing left to do, so turns refresh the pane.
|
|
254
|
+
isAutoOpened = true
|
|
252
255
|
void refreshWork($)
|
|
253
256
|
await $.ui.open({
|
|
254
257
|
id: PANE,
|
|
@@ -152,7 +152,15 @@ function fakeWorld(
|
|
|
152
152
|
opened.push(e.id)
|
|
153
153
|
return { value: { isPlaced: true } }
|
|
154
154
|
})
|
|
155
|
-
on("ui.panes", () => ({
|
|
155
|
+
on("ui.panes", () => ({
|
|
156
|
+
value: opened.map((id) => ({
|
|
157
|
+
id,
|
|
158
|
+
title: "Work",
|
|
159
|
+
isShown: true,
|
|
160
|
+
isFocused: false,
|
|
161
|
+
isPlaced: true,
|
|
162
|
+
})),
|
|
163
|
+
}))
|
|
156
164
|
on("command.register", (_$, e) => ({ value: { command: e.name } }))
|
|
157
165
|
on("session.start", (_$, e) => ({ cwd: e.cwd }))
|
|
158
166
|
return prompts
|
|
@@ -304,3 +312,50 @@ test("the picker and /resume open the pane once the resumed turns load", async (
|
|
|
304
312
|
await clock.advance(5_000)
|
|
305
313
|
expect(opened).toEqual(["work-items"])
|
|
306
314
|
})
|
|
315
|
+
|
|
316
|
+
test("each turn summarizes again while the pane is open, not once it closes", async ($, on) => {
|
|
317
|
+
const clock = mock.clock(on)
|
|
318
|
+
const opened = ["work-items"]
|
|
319
|
+
const prompts = fakeWorld(on, opened)
|
|
320
|
+
on("turn.complete", (_$, e) => ({ text: e.answer }))
|
|
321
|
+
const turn = {
|
|
322
|
+
answer: "",
|
|
323
|
+
durationMs: 1,
|
|
324
|
+
isAborted: false,
|
|
325
|
+
reason: "answer",
|
|
326
|
+
} as const
|
|
327
|
+
await $.turn.complete({ ...turn, turnId: "t1" })
|
|
328
|
+
await clock.settle()
|
|
329
|
+
await $.turn.complete({ ...turn, turnId: "t2" })
|
|
330
|
+
await clock.settle()
|
|
331
|
+
expect(prompts.length).toBe(2)
|
|
332
|
+
opened.length = 0
|
|
333
|
+
await $.turn.complete({ ...turn, turnId: "t3" })
|
|
334
|
+
await clock.settle()
|
|
335
|
+
expect(prompts.length).toBe(2)
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
test("a resumed session with nothing to show still summarizes each turn once /work opens the pane", async ($, on) => {
|
|
339
|
+
const clock = mock.clock(on)
|
|
340
|
+
const opened: string[] = []
|
|
341
|
+
const prompts = fakeWorld(on, opened, [message("hello")], (argv) =>
|
|
342
|
+
argv[0] === "gh" ? '{"data":{"repository":{}}}' : undefined,
|
|
343
|
+
)
|
|
344
|
+
on("turn.complete", (_$, e) => ({ text: e.answer }))
|
|
345
|
+
fakeSession(on, { id: "a", turns: 4 })
|
|
346
|
+
await $.session.start(START)
|
|
347
|
+
await clock.settle()
|
|
348
|
+
expect(opened).toEqual([])
|
|
349
|
+
await $.command.run({ command: "work", args: "" })
|
|
350
|
+
await clock.settle()
|
|
351
|
+
const before = prompts.length
|
|
352
|
+
await $.turn.complete({
|
|
353
|
+
answer: "",
|
|
354
|
+
durationMs: 1,
|
|
355
|
+
isAborted: false,
|
|
356
|
+
reason: "answer",
|
|
357
|
+
turnId: "t1",
|
|
358
|
+
})
|
|
359
|
+
await clock.settle()
|
|
360
|
+
expect(prompts.length).toBe(before + 1)
|
|
361
|
+
})
|