jackherd 1.18.2 → 1.20.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 +23 -0
- package/INSTALL.md +1 -1
- package/README.md +30 -25
- package/assets/icons/model.svg +1 -0
- package/dist/{chunk-N5FAOQZL.js → chunk-JDO6O3UO.js} +52 -2
- package/dist/config.d.ts +16 -2
- package/dist/config.js +25 -4
- package/dist/plan-format.d.ts +84 -1
- package/dist/plan-format.js +85 -1
- package/docs/plan-contract.md +8 -2
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/install.sh +1 -1
- package/kit/hooks/hooks/approval-gate/decision.ts +10 -11
- package/kit/hooks/hooks/approval-gate/run.ts +23 -3
- package/kit/hooks/hooks/browser-guard/decision.ts +7 -0
- package/kit/hooks/hooks/browser-guard/index.ts +50 -0
- package/kit/hooks/hooks/plain-question/arguments.ts +14 -1
- package/kit/hooks/hooks/plain-question/card.ts +102 -0
- package/kit/hooks/hooks/plain-question/form.ts +25 -9
- package/kit/hooks/hooks/plain-question/index.ts +20 -25
- package/kit/hooks/hooks/plain-question/recommendation.ts +6 -6
- package/kit/hooks/hooks/return-brief/decision.ts +29 -0
- package/kit/hooks/hooks/return-brief/index.ts +37 -0
- package/kit/hooks/hooks/return-brief/settings.ts +11 -0
- package/kit/hooks/hooks/shared/coordinator-call.ts +25 -0
- package/kit/hooks/hooks/shared/transcript.ts +9 -0
- package/kit/hooks/hooks.json +18 -1
- package/kit/review/agents/security-audit.md +1 -1
- package/kit/workflow/agents/browser-check.md +29 -0
- package/kit/workflow/skills/implement/SKILL.md +5 -3
- package/kit/workflow/skills/orchestrate/SKILL.md +36 -14
- package/kit/workflow/skills/orchestrate/references/acceptance.md +10 -9
- package/kit/workflow/skills/orchestrate/references/coordinator-clear.md +45 -0
- package/kit/workflow/skills/orchestrate/references/coordinator-handover.md +15 -12
- package/kit/workflow/skills/orchestrate/references/start-lane.md +3 -3
- package/kit/workflow/skills/plan/SKILL.md +2 -0
- package/kit/workflow/skills/plan/references/template-English.md +8 -0
- package/kit/workflow/skills/plan/references/template-German.md +8 -0
- package/package.json +1 -1
- package/schemas/jackherd.schema.json +21 -0
- package/src/brief/command.ts +45 -0
- package/src/brief/lane-brief.ts +51 -0
- package/src/brief/lane-facts.ts +64 -0
- package/src/check/skill-budget-defaults.ts +13 -2
- package/src/cli.ts +1 -0
- package/src/config/schema.ts +24 -0
- package/src/lane/command.ts +3 -1
- package/src/lane/port-command.ts +37 -0
- package/src/lane/port.ts +56 -0
- package/src/lane-prompt/texts.ts +2 -2
- package/src/plan-contract/command.ts +1 -0
- package/src/plan-contract/contract.ts +28 -5
- package/src/plan-format/index.ts +1 -0
- package/src/plan-format/plan-head.ts +131 -0
- package/src/plans/active-tasks.ts +8 -1
- package/src/plans/click-hint.ts +2 -1
- package/src/plans/focus-overlay.ts +5 -2
- package/src/plans/folded-detail.ts +47 -0
- package/src/plans/lane-line.ts +86 -0
- package/src/plans/lane-reason.ts +24 -4
- package/src/plans/layout-phase.ts +4 -20
- package/src/plans/layout-summary.ts +56 -45
- package/src/plans/layout.ts +178 -44
- package/src/plans/port-listening.ts +25 -0
- package/src/plans/selection.ts +11 -0
- package/src/plans/settings.ts +4 -0
- package/src/plans/snapshot.ts +27 -9
- package/src/plans/task-checklist.ts +57 -0
- package/src/plans/texts.ts +34 -9
- package/src/resume/command.ts +40 -6
- package/src/resume/coordinator-read.ts +136 -0
- package/src/resume/state.ts +163 -0
- package/src/resume/texts.ts +6 -0
- package/src/sync/render.ts +5 -1
- package/src/vocabulary/index.ts +74 -0
- package/src/watch/clear-moment.ts +228 -0
- package/src/watch/command.ts +92 -8
- package/src/watch/lane-report.ts +5 -1
- package/src/watch/reference-paths.ts +8 -4
- package/src/watch/watch-events.ts +13 -0
|
@@ -30,17 +30,17 @@ const markedGuardReason = (labels: readonly string[], { mark, guardedLabels }: R
|
|
|
30
30
|
: `Option "${label}": a guard reads "${withoutMark(label, mark)}" word for word; drop "${mark}" from it.`
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/**
|
|
33
|
+
/** A mark on a later option while the first carries none: the recommendation goes first. */
|
|
34
34
|
const firstMarkReason = (question: string, labels: readonly string[], mark: string): string | null => {
|
|
35
35
|
const [first, ...rest] = labels
|
|
36
36
|
if (first !== undefined && isMarked(first, mark)) return null
|
|
37
37
|
const later = rest.find((label) => isMarked(label, mark))
|
|
38
38
|
return later === undefined
|
|
39
|
-
?
|
|
39
|
+
? null
|
|
40
40
|
: `Question "${shorten(question)}": the recommendation goes first; move "${later}" to the top.`
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/** A single choice has one recommendation: the first option
|
|
43
|
+
/** A single choice has at most one recommendation: marked, it is the first option and no other carries the mark. */
|
|
44
44
|
const singleChoiceReason = (question: string, labels: readonly string[], mark: string): string | null => {
|
|
45
45
|
const firstReason = firstMarkReason(question, labels, mark)
|
|
46
46
|
const second = labels.slice(1).find((label) => isMarked(label, mark))
|
|
@@ -62,9 +62,9 @@ const questionReason = (question: Record<string, unknown>, words: Recommendation
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
|
-
* The reason to send a dialog back because a question
|
|
66
|
-
*
|
|
67
|
-
*
|
|
65
|
+
* The reason to send a dialog back because a question marks a recommendation badly: the mark stands at the end of the
|
|
66
|
+
* label of the first option, on no other, and at most once; null when every question does. A recommendation is
|
|
67
|
+
* voluntary, so a question without a mark passes. A label a guard reads word for word must not carry a mark.
|
|
68
68
|
*/
|
|
69
69
|
export const recommendationReasonOf = (toolInput: unknown, words: RecommendationWords): string | null => {
|
|
70
70
|
if (words.mark === '') return null
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const MINUTE_MS = 60_000
|
|
2
|
+
|
|
3
|
+
/** A prompt written this close to `now` is the one being submitted, which the transcript may already hold. */
|
|
4
|
+
const CURRENT_PROMPT_MS = 10_000
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_BRIEF_AFTER_MINUTES = 60
|
|
7
|
+
|
|
8
|
+
export type ReturnInput = {
|
|
9
|
+
/** When each prompt of the session was written, in milliseconds since the epoch. */
|
|
10
|
+
promptTimes: readonly number[]
|
|
11
|
+
now: number
|
|
12
|
+
/** Minutes of silence after which the user counts as returned; 0 turns the hook off. */
|
|
13
|
+
afterMinutes: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The minutes since the user's last earlier prompt; null when there is none (a first prompt is no return). */
|
|
17
|
+
export const minutesAway = (promptTimes: readonly number[], now: number): number | null => {
|
|
18
|
+
const earlier = promptTimes.filter((time) => time < now - CURRENT_PROMPT_MS)
|
|
19
|
+
return earlier.length === 0 ? null : Math.floor((now - Math.max(...earlier)) / MINUTE_MS)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The context for the coordinator when the user comes back after `afterMinutes` or more; null otherwise. */
|
|
23
|
+
export const returnContext = ({ promptTimes, now, afterMinutes }: ReturnInput): string | null => {
|
|
24
|
+
if (afterMinutes <= 0) return null
|
|
25
|
+
const away = minutesAway(promptTimes, now)
|
|
26
|
+
return away === null || away < afterMinutes
|
|
27
|
+
? null
|
|
28
|
+
: `The user was away for ${away} minutes. Before you answer, run \`jh brief\` and put its lines to the user in the language you reply in. Report only: the open questions it counts wait for their own dialog.`
|
|
29
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// UserPromptSubmit hook of Claude Code, shipped by jackherd. When the user comes back to the coordinator after
|
|
2
|
+
// `plans.briefAfterMinutes` (default 60) without a prompt, it adds the context to show the situation with `jh brief`
|
|
3
|
+
// first. It only adds context: every failure, a lane, an absent transcript or a first prompt ends in exit 0 without
|
|
4
|
+
// output, and a prompt is never blocked.
|
|
5
|
+
import { readFileSync } from 'node:fs'
|
|
6
|
+
import { isCoordinatorCall } from '../shared/coordinator-call'
|
|
7
|
+
import { isRecord } from '../shared/json'
|
|
8
|
+
import { promptTimesOf } from '../shared/transcript'
|
|
9
|
+
import { returnContext } from './decision'
|
|
10
|
+
import { briefAfterMinutes } from './settings'
|
|
11
|
+
|
|
12
|
+
const HOOK_EVENT = 'UserPromptSubmit'
|
|
13
|
+
|
|
14
|
+
const contextFor = (input: unknown): string | null => {
|
|
15
|
+
if (!isRecord(input) || typeof input.cwd !== 'string' || typeof input.transcript_path !== 'string') return null
|
|
16
|
+
const afterMinutes = briefAfterMinutes(process.env.CLAUDE_PROJECT_DIR ?? input.cwd)
|
|
17
|
+
if (afterMinutes <= 0) return null
|
|
18
|
+
const context = returnContext({
|
|
19
|
+
promptTimes: promptTimesOf(readFileSync(input.transcript_path, 'utf8')),
|
|
20
|
+
now: Date.now(),
|
|
21
|
+
afterMinutes,
|
|
22
|
+
})
|
|
23
|
+
return context !== null && isCoordinatorCall(input.cwd, input.session_id) ? context : null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const decide = async (): Promise<string | null> => {
|
|
27
|
+
try {
|
|
28
|
+
return contextFor(JSON.parse(await Bun.stdin.text()))
|
|
29
|
+
} catch {
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const context = await decide()
|
|
35
|
+
if (context !== null)
|
|
36
|
+
console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: HOOK_EVENT, additionalContext: context } }))
|
|
37
|
+
process.exit(0)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { readConfig, sectionOf } from '../shared/config'
|
|
3
|
+
import { DEFAULT_BRIEF_AFTER_MINUTES } from './decision'
|
|
4
|
+
|
|
5
|
+
const isMinutes = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value) && value >= 0
|
|
6
|
+
|
|
7
|
+
/** `plans.briefAfterMinutes` of the first config that sets it to a number (project before home), else the default. */
|
|
8
|
+
export const briefAfterMinutes = (projectDirectory: string, home: string = homedir()): number =>
|
|
9
|
+
[readConfig([projectDirectory]), readConfig([home])]
|
|
10
|
+
.map((config) => sectionOf(config, 'plans').briefAfterMinutes)
|
|
11
|
+
.find(isMinutes) ?? DEFAULT_BRIEF_AFTER_MINUTES
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { boardLocationOf } from './boards'
|
|
4
|
+
import { mainCheckoutOf } from './checkout'
|
|
5
|
+
import { readConfig } from './config'
|
|
6
|
+
import { isCoordinatorAgent } from './lane'
|
|
7
|
+
import { ownPaneAgent } from './own-pane'
|
|
8
|
+
|
|
9
|
+
/** Whether the hook was called for the coordinator's session; any failure or a project without a board answers no. */
|
|
10
|
+
export const isCoordinatorCall = (cwd: string, session: unknown): boolean => {
|
|
11
|
+
try {
|
|
12
|
+
const main = mainCheckoutOf(cwd)
|
|
13
|
+
const location = boardLocationOf(readConfig([main]))
|
|
14
|
+
if (location === null) return false
|
|
15
|
+
const { worktreeDirectory } = location
|
|
16
|
+
return isCoordinatorAgent({
|
|
17
|
+
agent: ownPaneAgent(),
|
|
18
|
+
session,
|
|
19
|
+
main,
|
|
20
|
+
isLane: (name) => existsSync(join(main, worktreeDirectory, name)),
|
|
21
|
+
})
|
|
22
|
+
} catch {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -13,6 +13,7 @@ type Entry = {
|
|
|
13
13
|
type?: unknown
|
|
14
14
|
isMeta?: unknown
|
|
15
15
|
promptId?: unknown
|
|
16
|
+
timestamp?: unknown
|
|
16
17
|
message?: { content?: unknown }
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -87,3 +88,11 @@ export const readTurn = (jsonl: string, promptId: string | undefined): Turn => {
|
|
|
87
88
|
.map(({ name }) => name),
|
|
88
89
|
}
|
|
89
90
|
}
|
|
91
|
+
|
|
92
|
+
/** When each prompt the user typed was written, in milliseconds since the epoch, in order; a prompt without a readable time is skipped. */
|
|
93
|
+
export const promptTimesOf = (jsonl: string): number[] =>
|
|
94
|
+
parseEntries(jsonl).flatMap((entry) => {
|
|
95
|
+
if (promptText(entry) === undefined || typeof entry.timestamp !== 'string') return []
|
|
96
|
+
const time = Date.parse(entry.timestamp)
|
|
97
|
+
return Number.isNaN(time) ? [] : [time]
|
|
98
|
+
})
|
package/kit/hooks/hooks.json
CHANGED
|
@@ -15,6 +15,15 @@
|
|
|
15
15
|
"timeout": 10
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
|
+
"browser-guard": {
|
|
19
|
+
"event": "PreToolUse",
|
|
20
|
+
"matcher": "mcp__claude-in-chrome__.*",
|
|
21
|
+
"handler": {
|
|
22
|
+
"type": "command",
|
|
23
|
+
"command": "cd \"$CLAUDE_PROJECT_DIR\" && bun .claude/hooks/browser-guard/index.ts",
|
|
24
|
+
"timeout": 10
|
|
25
|
+
}
|
|
26
|
+
},
|
|
18
27
|
"format-on-edit": {
|
|
19
28
|
"event": "PostToolUse",
|
|
20
29
|
"matcher": "Edit|Write",
|
|
@@ -83,7 +92,15 @@
|
|
|
83
92
|
"matcher": "AskUserQuestion",
|
|
84
93
|
"handler": {
|
|
85
94
|
"type": "command",
|
|
86
|
-
"command": "cd \"$CLAUDE_PROJECT_DIR\" && bun .claude/hooks/plain-question/index.ts '{{kit.question.recommended}}' '{{kit.approval.allow}}' '{{kit.approval.deny}}' '{{kit.guard.selfMergeLabel}}' '{{kit.guard.standingApproval.approve}}'",
|
|
95
|
+
"command": "cd \"$CLAUDE_PROJECT_DIR\" && bun .claude/hooks/plain-question/index.ts '{{kit.question.recommended}}' '{{kit.approval.allow}}' '{{kit.approval.deny}}' '{{kit.guard.selfMergeLabel}}' '{{kit.guard.standingApproval.approve}}' '{{kit.card.topic}}' '{{kit.card.consequences}}' '{{kit.card.ifUndecided}}' '{{kit.card.readMore}}'",
|
|
96
|
+
"timeout": 10
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"return-brief": {
|
|
100
|
+
"event": "UserPromptSubmit",
|
|
101
|
+
"handler": {
|
|
102
|
+
"type": "command",
|
|
103
|
+
"command": "cd \"$CLAUDE_PROJECT_DIR\" && bun .claude/hooks/return-brief/index.ts",
|
|
87
104
|
"timeout": 10
|
|
88
105
|
}
|
|
89
106
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: security-audit
|
|
3
3
|
description: 'Use when a change touches auth, tenant isolation, access policies, API keys, secrets, user input, database queries, message payloads or infrastructure: OWASP Top 10, secrets exposure, authz, input validation, injection, path traversal.'
|
|
4
|
-
model: {{kit.model.
|
|
4
|
+
model: {{kit.model.plan}}
|
|
5
5
|
tools: Read, Grep, Glob
|
|
6
6
|
user-invocable: false
|
|
7
7
|
---
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: browser-check
|
|
3
|
+
description: 'Looks at a running page in the Claude in Chrome extension and reports findings with saved screenshots. Use when: a change needs a look in the real browser (acceptance of a lane, a visual or console check). Reads and looks only.'
|
|
4
|
+
model: {{kit.model.review}}
|
|
5
|
+
tools: Read, mcp__claude-in-chrome__tabs_context_mcp, mcp__claude-in-chrome__tabs_create_mcp, mcp__claude-in-chrome__tabs_close_mcp, mcp__claude-in-chrome__navigate, mcp__claude-in-chrome__computer, mcp__claude-in-chrome__read_page, mcp__claude-in-chrome__get_page_text, mcp__claude-in-chrome__find, mcp__claude-in-chrome__read_console_messages, mcp__claude-in-chrome__read_network_requests
|
|
6
|
+
user-invocable: false
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
You look at a page in the user's Chrome and report what you saw. The page stays exactly as it is.
|
|
10
|
+
|
|
11
|
+
## Task
|
|
12
|
+
|
|
13
|
+
The prompt names the port or address, the path to open and what to check. Open a new tab of your own, never an existing one, and close it when you are done.
|
|
14
|
+
|
|
15
|
+
## Rules
|
|
16
|
+
|
|
17
|
+
- The extension acts as the user, with their real login: read and look only. Navigate, scroll, hover, read the page, the console and the network; a click that only opens or shows is yours. A click or an entry that would write (submit, save, delete, send, confirm) is not: name the step in your report and stop there. Never run against a shared environment unless the prompt says so.
|
|
18
|
+
- One Chrome serves all lanes. Work one check at a time and close your tab afterwards.
|
|
19
|
+
- A tab in a background window renders the DOM and takes screenshots, but anything waiting on animation frames stays blank there. Report that as the reason, not as a finding about the page.
|
|
20
|
+
- Take screenshots with `save_to_disk`, at least one per path and one per step the prompt asks to see, and keep the saved paths. A screenshot that times out gets one more try.
|
|
21
|
+
- Wait for data before you judge: poll for real content instead of reading an empty page right after navigation, for up to 60 seconds unless the prompt says otherwise, then report a timeout.
|
|
22
|
+
|
|
23
|
+
## Report
|
|
24
|
+
|
|
25
|
+
Return plain text, no screenshot inline:
|
|
26
|
+
|
|
27
|
+
- Per step: what you opened, what you saw, and the finding (matches the ask, or what differs).
|
|
28
|
+
- The absolute path of every saved screenshot, one per line, with what it shows.
|
|
29
|
+
- Steps you did not run because they would write, and why you could not look (blank page, login wall, timeout).
|
|
@@ -54,7 +54,7 @@ Write code and tests with Write and Edit, never through a here-document or a scr
|
|
|
54
54
|
|
|
55
55
|
## Step 3: Verify
|
|
56
56
|
|
|
57
|
-
After each changed file: `{{kit.command.typecheck}}`
|
|
57
|
+
After each changed file: `{{kit.command.typecheck}}` and `{{kit.command.lint}}` clean, no dead code.
|
|
58
58
|
|
|
59
59
|
## Step 4: Write Tests
|
|
60
60
|
|
|
@@ -62,7 +62,7 @@ Follow the decision tree in `.claude/rules/kit/testing.md`; run the tests with `
|
|
|
62
62
|
|
|
63
63
|
## Step 4b: Look at It
|
|
64
64
|
|
|
65
|
-
A change a user sees (a page, a route, a redirect, a printed document) is looked at in the running app before `/pre-commit`: unit tests prove the pieces, only the running app proves they meet. Use a temporary end-to-end spec or the browser, a screenshot per state, and report what each shows; delete the temporary spec before the commit unless the plan asks for one. No visible change → skip
|
|
65
|
+
A change a user sees (a page, a route, a redirect, a printed document) is looked at in the running app before `/pre-commit`: unit tests prove the pieces, only the running app proves they meet. Use a temporary end-to-end spec or the browser, a screenshot per state, and report what each shows; delete the temporary spec before the commit unless the plan asks for one. No visible change → skip.
|
|
66
66
|
|
|
67
67
|
## Step 5: arc42 and Architecture
|
|
68
68
|
|
|
@@ -74,7 +74,7 @@ Once tests are green, types clean, the change looked at and Step 5 done: call `/
|
|
|
74
74
|
|
|
75
75
|
Ask every question with a fixed set of answers through the `AskUserQuestion` tool, in {{kit.responseLanguage}}.
|
|
76
76
|
|
|
77
|
-
The report at the end is
|
|
77
|
+
The report at the end is five lines, each opening with its field: `{{kit.report.goal}}:`, `{{kit.report.phase}} x/y:`, `{{kit.report.result}}:` (diff stat, a figure per check), `{{kit.report.needsYou}}:` (else `nothing`), `{{kit.report.next}}:`; then the questions; the rest is in the plan. Each question carries the decision card, four lines opening with `{{kit.card.topic}}:`, `{{kit.card.consequences}}:`, `{{kit.card.ifUndecided}}:` and `{{kit.card.readMore}}:` (a link or path): the situation, the problem and what each option leads to, in everyday words. Mark each question `blocking` or `not blocking`. A blocking one stops the step it decides; a not blocking one leaves a `TODO` at its spot and the work goes on, with neither your recommendation nor another option built as decided.
|
|
78
78
|
|
|
79
79
|
### Git commands run one at a time
|
|
80
80
|
|
|
@@ -84,6 +84,8 @@ The report at the end is short: one line of diff stat, one line per check with i
|
|
|
84
84
|
|
|
85
85
|
Runs once the phase's review gate is ticked off, committed or not (a lane under `/orchestrate` commits only when its pull request is ready), and only then — never in the middle of a task.
|
|
86
86
|
|
|
87
|
+
Before the next phase or the hand-over, update the plan's `## {{kit.heading.forYou}}` head: `Phase x/y` names the next phase with an open box (the last one once all are done), then open decisions, what the user can try, next step; done when `jh plan check <plan>` prints `head: present`. A plan without a head gets one at its last phase boundary, not before.
|
|
88
|
+
|
|
87
89
|
1. Read the fill of your own context from the log, not from memory:
|
|
88
90
|
|
|
89
91
|
```bash
|
|
@@ -140,7 +140,7 @@ On wake-up:
|
|
|
140
140
|
| `gh unavailable` | Tell the user once; herdr watching continues |
|
|
141
141
|
| `lane <name> alert` | Send the lane once the request for its hand-over at the end of its current phase: [`references/lane-handover.md`](references/lane-handover.md) |
|
|
142
142
|
| `approval <name> <id> <tool> · <call>` | A call of the lane waits for the user's approval (**Approvals**, below) |
|
|
143
|
-
| `coordinator warn` / `coordinator alert` | § 8, for yourself |
|
|
143
|
+
| `coordinator clear` / `coordinator warn` / `coordinator alert` | § 8, for yourself |
|
|
144
144
|
| `lane added: <name>` | Check that it has its row (§ 2 step 5); a name `<lane>-<purpose>` without a worktree is a helper agent of that lane and needs no row. One the lane did not announce goes to the user |
|
|
145
145
|
| `kit updated <old> → <new>` | The watcher ended. After the `jh update` call has finished (with `(source changed)` there is no call to wait for), read the change of this skill and of each reference the next lines name, by the diff or file each names; read this skill file again (no session restart) only when the line says `diff unavailable`; `orchestrate skill unchanged` needs no reading. Then start the stream again |
|
|
146
146
|
| an event line ending in `· read <path>` | A rare event (`feedback`, `preview`, `source`, `parallel`, `deploy`, `due`, `stalled`, a lane's `warn`, …): read the file it names and handle it as it says; the `parallel tasks` section of [`references/events.md`](references/events.md) also before you switch `watch.parallelTasks.auto` on |
|
|
@@ -148,6 +148,9 @@ On wake-up:
|
|
|
148
148
|
| `superseded by <pid>` / `stopped by SIGTERM` | Nothing for `superseded`. After `stopped by SIGTERM` from outside, start it again |
|
|
149
149
|
| `watcher stopped: <reason>` | Tell the user, fix the cause, then start it again |
|
|
150
150
|
|
|
151
|
+
**A look in the browser goes to the `browser-check` agent:** screenshots in your own context stay until the next
|
|
152
|
+
`/clear`. A look that needs page scripts goes to the lane.
|
|
153
|
+
|
|
151
154
|
**A decision of the user goes into the file at once.** Whatever the user decides about a plan or the
|
|
152
155
|
board (a phase dropped or postponed, the order changed, an approval with its scope, an answer) you write
|
|
153
156
|
into the plan or the board in the same turn, with the date and "User". **A date or time in a record is read,
|
|
@@ -177,6 +180,9 @@ the turn the answer arrives (also a "no") or the thing is withdrawn; an empty st
|
|
|
177
180
|
waiting on the answer, a source's report and what needs no row: [`references/waiting.md`](references/waiting.md).
|
|
178
181
|
A line `Ask me the open … now (jh waiting).` from the herd: [`references/herd-ask.md`](references/herd-ask.md).
|
|
179
182
|
|
|
183
|
+
**The user comes back after a long absence** (the `return-brief` hook adds the context "The user was away for n
|
|
184
|
+
minutes"): show the `jh brief` lines, so they see each lane's goal, status and what it needs from them.
|
|
185
|
+
|
|
180
186
|
**A new idea during a release** (while a plan's phases run toward one release) is a line in the plan's
|
|
181
187
|
`## Follow-ups` section, with its origin, and one line to the user: no dialog, no new phase. It enters the
|
|
182
188
|
release only when the release would otherwise be wrong (a bug, a false statement, a security hole) or the
|
|
@@ -194,8 +200,8 @@ Jack has seen. Before `AskUserQuestion`, every option of the lane's question blo
|
|
|
194
200
|
--topic <topic>`, the only way to answer a lane's question yourself. A refusal sends the question to
|
|
195
201
|
the user.
|
|
196
202
|
4. **Own words are evidence.** A sentence Jack adds is either the lane's evidence or checked by Jack.
|
|
197
|
-
5. **Context the user can follow
|
|
198
|
-
to the lane.
|
|
203
|
+
5. **Context the user can follow**: the decision card (**Short questions**, below). A question without
|
|
204
|
+
its four fields, or with options that carry only `file:line`, goes back to the lane.
|
|
199
205
|
|
|
200
206
|
Done when each relayed premise carries its source and only open decisions remain.
|
|
201
207
|
|
|
@@ -216,22 +222,37 @@ Silence is no answer: a collected question stays open until the user answered it
|
|
|
216
222
|
|
|
217
223
|
**Short questions.** A dialog holds the decision and nothing else. The
|
|
218
224
|
`plain-question` hook sends back a question of more than 15 words or with a path, flag or code in it, a label
|
|
219
|
-
of more than five words, a description past one sentence of about twelve,
|
|
220
|
-
(`the architecture docs (P7)`)
|
|
221
|
-
the hook:
|
|
225
|
+
of more than five words, a description past one sentence of about twelve, a plan code without its words
|
|
226
|
+
(`the architecture docs (P7)`), and a dialog that lacks the **decision card**; a dialog that offers a
|
|
227
|
+
guard's label is checked only for the paw. Beyond the hook:
|
|
222
228
|
|
|
223
229
|
- No number in the question, unless it is the decision itself; two or three options, mostly.
|
|
224
|
-
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
230
|
+
- A recommendation only where you would honestly give one, and then always: first option, ending its label with
|
|
231
|
+
`{{kit.question.recommended}}`, at most one in a single choice. With none, no option carries a mark.
|
|
232
|
+
- The context comes with it, in the dialog, as the decision card: four lines, each opening with its field
|
|
233
|
+
(`{{kit.card.topic}}`, `{{kit.card.consequences}}`, `{{kit.card.ifUndecided}}`, `{{kit.card.readMore}}`),
|
|
234
|
+
only what the decision needs: the situation, the problem and what each option leads to, in everyday words,
|
|
235
|
+
with any term the user may not know explained. The hook does not see the chat before the dialog, so the card
|
|
236
|
+
stands in each option's `preview`: its own `{{kit.card.consequences}}` line in every option, the other three
|
|
237
|
+
lines once, in the first option's. A multiple choice has no `preview`: its question carries the four lines,
|
|
238
|
+
up to 60 words. Consequences name what can be checked (diff lines, files, an amount, downtime), not a summary.
|
|
239
|
+
The link is one you read, never an invented one. Details come on request.
|
|
240
|
+
- A lane's question you pass on names the lane in backticks in its `{{kit.card.topic}}` line
|
|
241
|
+
(``{{kit.card.topic}}: lane `<lane>` asks …``, in the language of the replies), not in the question;
|
|
242
|
+
**Approvals** keeps its own form.
|
|
229
243
|
- A plan item in the chat and in a waiting row is named by what it is, as in the dialog.
|
|
230
244
|
|
|
231
245
|
```text
|
|
232
246
|
Question: Retry the failed check, or leave it?
|
|
233
247
|
- Retry it (recommended): runs the check again
|
|
248
|
+
preview:
|
|
249
|
+
{{kit.card.topic}}: the type check failed once on an unchanged diff.
|
|
250
|
+
{{kit.card.consequences}}: a retry takes about 8 minutes and changes no file.
|
|
251
|
+
{{kit.card.ifUndecided}}: the pull request waits.
|
|
252
|
+
{{kit.card.readMore}}: <link to the failed run>
|
|
234
253
|
- Leave it: the pull request waits
|
|
254
|
+
preview:
|
|
255
|
+
{{kit.card.consequences}}: the merge stays held until the check is green.
|
|
235
256
|
```
|
|
236
257
|
|
|
237
258
|
**A draft for review** (a README, a page, a plan a lane wrote for the user to read, or a text you wrote for the
|
|
@@ -339,8 +360,9 @@ After its last PR is merged, follow
|
|
|
339
360
|
- **A lane's hand-over:** its request on `lane <name> alert`, and a lane `done` or `idle` whose worktree holds
|
|
340
361
|
`.claude/handover/<lane>.md` with the line `{{kit.word.openSubagents}}: 0`:
|
|
341
362
|
[`references/lane-handover.md`](references/lane-handover.md).
|
|
342
|
-
- **For yourself:** on `coordinator
|
|
343
|
-
|
|
344
|
-
|
|
363
|
+
- **For yourself:** on `coordinator clear`, clear without asking; on `coordinator warn`, work on until that line
|
|
364
|
+
comes: [`references/coordinator-clear.md`](references/coordinator-clear.md). On
|
|
365
|
+
`coordinator alert`, hand yourself over: [`references/coordinator-handover.md`](references/coordinator-handover.md).
|
|
366
|
+
After a restart, a hand-over or a clear, compare `CronList` with the board's appointments as those files say. A status
|
|
345
367
|
line without a value (the reason in brackets, such as `no agent named <name>`, or the error it hit) is
|
|
346
368
|
no value: tell the user in one line and never read it as `ok`.
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
Read when a lane with a visible change reports ready, before you put its PR question to the user
|
|
4
4
|
(`/orchestrate` § 5 step 2).
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
lane's
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
Send the look to the `browser-check` agent. Its prompt names the lane's port, the path the phase changed, what to
|
|
7
|
+
check, above all what the lane's spec cannot show (the user's account, role, theme), and the surfaces the project supplement lists as blank in a background window. The agent returns findings as text and the paths of the
|
|
8
|
+
screenshots it saved: forward the files to the user with `SendUserFile` without looking at them yourself, and send
|
|
9
|
+
each finding back to the lane instead of asking about the PR. A session without that tool gives the user the files' absolute paths instead, unseen as well. Done when the user has the files or their paths and every finding
|
|
10
|
+
went to the lane.
|
|
10
11
|
|
|
11
|
-
- One Chrome serves all lanes:
|
|
12
|
-
- A
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
runs against a shared environment without their approval.
|
|
12
|
+
- One Chrome serves all lanes: run one `browser-check` at a time.
|
|
13
|
+
- A report that it could not look (blank in a background window, login wall, timeout) is no finding for the lane:
|
|
14
|
+
ask the user to bring the window up or log in, then send the look again.
|
|
15
|
+
- A step that writes comes back to you and goes to the user first: the user does it in their browser, or the lane
|
|
16
|
+
in its own spec. It never runs against a shared environment without their approval.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# § 8: clearing your context at a quiet moment
|
|
2
|
+
|
|
3
|
+
Read on the watcher line `coordinator clear <n>k`, on `coordinator warn`, and when a chat starts as
|
|
4
|
+
`/orchestrate <board> Cleared: …` in your reply language.
|
|
5
|
+
|
|
6
|
+
**The watcher decides the moment, you do not.** It sends `coordinator clear` when nothing is open: once per rise of
|
|
7
|
+
your fill above `plans.context.coordinator.clearTokens`, and once after a merge with the board written afterwards.
|
|
8
|
+
`coordinator warn`: work on until that line comes. `coordinator alert`:
|
|
9
|
+
[`coordinator-handover.md`](coordinator-handover.md); with both lines standing, `coordinator clear` wins, it needs
|
|
10
|
+
no note.
|
|
11
|
+
|
|
12
|
+
## On `coordinator clear`
|
|
13
|
+
|
|
14
|
+
1. **Secure what only the chat holds.** A decision of the user not yet in a plan or the board goes there now (§ 4);
|
|
15
|
+
a trap, a suspicion or a next step only the chat holds goes as one dated line into the board's history, an
|
|
16
|
+
instruction that holds only for now with the condition it ends on.
|
|
17
|
+
Write no note: what the files hold, nobody copies. A clear is no bundling moment: board lines stay in the main
|
|
18
|
+
checkout's board. Wait for your own background tasks and subagents to finish, the watcher's stream aside. A dialog
|
|
19
|
+
or a new report since the line means this moment has passed: work on. The next line comes after the next merge;
|
|
20
|
+
without one, `coordinator alert` hands you over.
|
|
21
|
+
2. **Read the list and get the line:** the `standing approvals` post of `jh resume --coordinator --board <board>`
|
|
22
|
+
names each line and end for step 3; `jh resume --clear-line --board <board>` prints the first input of the next
|
|
23
|
+
chat.
|
|
24
|
+
3. **Start the hand-over script** with it, as a plain Bash call, without `&`, `run_in_background` or `setsid`:
|
|
25
|
+
`bun .claude/skills/orchestrate/handover/index.ts --send "<the line>"`. Per standing approval
|
|
26
|
+
still holding, add `--board <board> --approval "<line number> <its end>"`;
|
|
27
|
+
it goes in as a hint and the new chat asks again. The script returns `hand-over started: pid <n>, log <path>`.
|
|
28
|
+
**End your turn right after it**, with one line to the user that the chat is about to clear. Any other output
|
|
29
|
+
(`hand-over not started: <reason>`) means no clear runs.
|
|
30
|
+
[`coordinator-handover.md`](coordinator-handover.md) covers what the script does once started, aborts included.
|
|
31
|
+
|
|
32
|
+
## In the new chat
|
|
33
|
+
|
|
34
|
+
1. Run `jh resume --coordinator --board <board>`. Each post lists its count and one line per entry with its place;
|
|
35
|
+
zero means none, `unknown` means it could not be read, so read that one yourself. Only the board, herdr and `gh`
|
|
36
|
+
can be `unknown`: an unreadable approvals or records directory counts as empty. Then read the newest dated lines
|
|
37
|
+
of the board's history: what the old chat secured stands there.
|
|
38
|
+
2. Start the watcher (§ 4). The old chat's stream outlives the clear, and starting yours ends it with `superseded by
|
|
39
|
+
<pid>`, nothing to do; background tasks of the old chat keep running too.
|
|
40
|
+
3. Approvals the watcher reported before the clear are not reported again: take the open ones from
|
|
41
|
+
the list and handle them (**Approvals**, § 4). Work through the rest of the list: a lane's open questions from the report it names; an `order without a report` waits for
|
|
42
|
+
that lane, nothing to send again; PRs, what waits on the user and appointments as § 4 and § 5 say. Timers survive
|
|
43
|
+
`/clear`: compare `CronList` with the appointments listed, create a missing one, delete one without a row. Done
|
|
44
|
+
when every listed lane has a started next action or a named reason to wait (§ 4), and every other post is handled
|
|
45
|
+
or zero.
|
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
# § 8: the coordinator hands itself over
|
|
2
2
|
|
|
3
|
-
Read on the watcher event `coordinator
|
|
4
|
-
`/orchestrate <board> Hand-over: …` or after a restart of your session.
|
|
3
|
+
Read on the watcher event `coordinator alert`, and when a chat starts as
|
|
4
|
+
`/orchestrate <board> Hand-over: …` or after a restart of your session. Clearing is the normal way
|
|
5
|
+
([`coordinator-clear.md`](coordinator-clear.md)); a hand-over with a note is for `coordinator alert`, when no quiet
|
|
6
|
+
moment came. `jh resume` starts a chat as
|
|
5
7
|
`/orchestrate <board> Restart after a crash: …` in your reply language: start the watcher (§ 4), compare the
|
|
6
8
|
alarms below, handle each lane as on wake-up; the resumed lanes it names have their message.
|
|
7
9
|
|
|
8
|
-
## The hand-over, at
|
|
10
|
+
## The hand-over, at your next stop
|
|
9
11
|
|
|
10
|
-
Hand yourself over to a fresh chat
|
|
11
|
-
the user awaited
|
|
12
|
+
Hand yourself over to a fresh chat as soon as no merge or rebase is half done, no dialog is open and no answer of
|
|
13
|
+
the user is awaited:
|
|
12
14
|
|
|
13
15
|
1. **Prepare.** Record your hand-over where the project supplement says, by default in the board's
|
|
14
|
-
`{{kit.heading.history}}` (dated as § 4 reads it): the
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
`{{kit.heading.history}}` (dated as § 4 reads it): the next step and the traps and suspicions no file holds. The note holds the hand-over only: a decision of the user that is not yet in a plan or the board
|
|
17
|
+
belongs there first (§ 4), not into the note; the state of the lanes and what waits on the user come from
|
|
18
|
+
`jh resume --coordinator`. Collected not blocking questions are named by the path of the
|
|
17
19
|
lane report that holds them, never copied. Board lines of § 4 not on `{{kit.baseBranch}}` yet go into a documentation PR now (the bundling moment). Write the prompt for the new chat to
|
|
18
|
-
`.claude/handover/coordinator.md`: what to read first, then the steps in order
|
|
19
|
-
|
|
20
|
+
`.claude/handover/coordinator.md`: what to read first, then the steps in order, the first of them
|
|
21
|
+
`jh resume --coordinator --board <board>`, whose list holds the approvals the watcher will not report again. The
|
|
22
|
+
file is read, not typed, so it may be long.
|
|
20
23
|
2. **Start the hand-over script** as a plain Bash call, without `&`, `run_in_background` or `setsid`:
|
|
21
24
|
`bun .claude/skills/orchestrate/handover/index.ts --send "/orchestrate <board> Hand-over: read .claude/handover/coordinator.md first and continue with the next step it names."`
|
|
22
25
|
Per standing approval still holding, add `--board <board> --approval "<line number> <its end>"`: its row
|
|
23
|
-
goes in as a hint; the new chat asks again ([`standing-approval.md`](standing-approval.md)); `claude --resume` keeps
|
|
24
|
-
with one line to the user that the chat is about to clear.
|
|
26
|
+
goes in as a hint; the new chat asks again ([`standing-approval.md`](standing-approval.md)); `claude --resume` keeps the approval. The script detaches itself and returns `hand-over started: pid <n>, log <path>`. **End your turn right after it**,
|
|
27
|
+
with one line to the user that the chat is about to clear. Any other output (`hand-over not started: <reason>`, for example a refused `--approval`) means no hand-over runs.
|
|
25
28
|
|
|
26
29
|
## What the script does
|
|
27
30
|
|
|
@@ -32,7 +32,7 @@ column and the plan's dependency table are met and step 1 finds nothing.
|
|
|
32
32
|
4. **Dependencies**, installed in the worktree's pane: the same file.
|
|
33
33
|
5. **Agent with the plan's model:**
|
|
34
34
|
`herdr agent start <lane> --kind claude --pane <pane> --timeout 120000 -- --model <model-id>`.
|
|
35
|
-
- **The lane's row comes first** in the board's `## {{kit.heading.lanes}}` table: lane, port, plan,
|
|
35
|
+
- **The lane's row comes first** in the board's `## {{kit.heading.lanes}}` table: lane, port (from `jh lane port`), plan,
|
|
36
36
|
phases, starts after, model. The phases cell starts with the phase id (`P10c …`), also for a sub-phase, and
|
|
37
37
|
the plan column links the plan once it exists, in the row of every lane that implements one of its
|
|
38
38
|
phases. Without the `lane-start-guard` hook, `grep -F "| \`<lane>\`" <board>` must print the row.
|
|
@@ -63,5 +63,5 @@ herdr pane run <new-pane> "PORT=<port> {{kit.command.dev}}"
|
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
The new pane starts in the lane's worktree; check with `/proc/<pid>/cwd` (macOS: `lsof -p <pid>`) that the server runs
|
|
66
|
-
there, and before stopping one by PID. Stop only a server you started;
|
|
67
|
-
|
|
66
|
+
there, and before stopping one by PID. Stop only a server you started; another's port
|
|
67
|
+
goes to the user.
|
|
@@ -30,6 +30,8 @@ A plan records decisions; it is not where they get taken. If the `## {{kit.headi
|
|
|
30
30
|
|
|
31
31
|
**Structure:** copy the template in [`references/template-{{kit.planLanguage}}.md`](references/template-{{kit.planLanguage}}.md) and fill it in. Every section stays, in its order, with its heading verbatim.
|
|
32
32
|
|
|
33
|
+
The `## {{kit.heading.forYou}}` head is what a person reads first, in everyday words, with `Phase 1/<number of phases>` in the status line; done when `jh plan check <plan>` prints `head: present`.
|
|
34
|
+
|
|
33
35
|
The closing documentation section reads `## N — …` with rows `N.1` to `N.5`: replace `N` with the number of that phase (`## 10 — …`, `10.1`). A literal `N` is no phase to the herd, which shows it as "not in the plan".
|
|
34
36
|
|
|
35
37
|
A checkbox moved on purpose to a later phase stays where it is, unticked, and ends in the target: `- [ ] Update the documentation → P9`. The herd counts it neither as open nor as done in its own phase and lists it as open in phase 9. The arrow stands only at the end of the line. Once a phase's tasks are ticked on the base branch, every box still open in it, gate boxes included, is ticked or moved on this way; `jh check` checks both.
|
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
**{{kit.heading.status}}:** Draft / In Progress / Done / Abandoned
|
|
5
5
|
**{{kit.heading.scope}}:** `affected/paths`
|
|
6
6
|
|
|
7
|
+
## {{kit.heading.forYou}}
|
|
8
|
+
|
|
9
|
+
- **What changes for you:** one sentence in everyday words
|
|
10
|
+
- **Status:** Phase 1/N (N = number of phases): name of the running one
|
|
11
|
+
- **Your open decisions:** which, or "none"
|
|
12
|
+
- **Try it now:** what, or "nothing"
|
|
13
|
+
- **Next step:** what, and who
|
|
14
|
+
|
|
7
15
|
## Problem / Context
|
|
8
16
|
|
|
9
17
|
What is the trigger? Why now? One paragraph.
|
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
**{{kit.heading.status}}:** Draft / In Progress / Done / Abandoned
|
|
5
5
|
**{{kit.heading.scope}}:** `betroffene/pfade`
|
|
6
6
|
|
|
7
|
+
## {{kit.heading.forYou}}
|
|
8
|
+
|
|
9
|
+
- **Was sich für dich ändert:** ein Satz in Alltagsworten
|
|
10
|
+
- **Stand:** Phase 1/N (N = Zahl der Phasen): Name der laufenden
|
|
11
|
+
- **Deine offenen Entscheidungen:** welche, oder „keine“
|
|
12
|
+
- **Schon ausprobieren:** was, oder „nichts“
|
|
13
|
+
- **Nächster Schritt:** was, und wer
|
|
14
|
+
|
|
7
15
|
## Problem / Kontext
|
|
8
16
|
|
|
9
17
|
Was ist der Auslöser? Warum jetzt? Ein Absatz.
|
package/package.json
CHANGED
|
@@ -147,6 +147,11 @@
|
|
|
147
147
|
"minFreeGb": {
|
|
148
148
|
"type": "number",
|
|
149
149
|
"exclusiveMinimum": 0
|
|
150
|
+
},
|
|
151
|
+
"basePort": {
|
|
152
|
+
"type": "integer",
|
|
153
|
+
"minimum": 1,
|
|
154
|
+
"maximum": 65535
|
|
150
155
|
}
|
|
151
156
|
},
|
|
152
157
|
"additionalProperties": false
|
|
@@ -221,6 +226,11 @@
|
|
|
221
226
|
"default": true,
|
|
222
227
|
"type": "boolean"
|
|
223
228
|
},
|
|
229
|
+
"briefAfterMinutes": {
|
|
230
|
+
"default": 60,
|
|
231
|
+
"type": "number",
|
|
232
|
+
"minimum": 0
|
|
233
|
+
},
|
|
224
234
|
"linkKinds": {
|
|
225
235
|
"default": [
|
|
226
236
|
{
|
|
@@ -314,6 +324,17 @@
|
|
|
314
324
|
"type": "integer",
|
|
315
325
|
"exclusiveMinimum": 0,
|
|
316
326
|
"maximum": 9007199254740991
|
|
327
|
+
},
|
|
328
|
+
"clearTokens": {
|
|
329
|
+
"type": "integer",
|
|
330
|
+
"exclusiveMinimum": 0,
|
|
331
|
+
"maximum": 9007199254740991
|
|
332
|
+
},
|
|
333
|
+
"clearAfterMergeTokens": {
|
|
334
|
+
"default": 150000,
|
|
335
|
+
"type": "integer",
|
|
336
|
+
"exclusiveMinimum": 0,
|
|
337
|
+
"maximum": 9007199254740991
|
|
317
338
|
}
|
|
318
339
|
},
|
|
319
340
|
"additionalProperties": false
|