jackherd 0.0.1 → 1.0.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 +61 -0
- package/LICENSE +21 -0
- package/NOTICE +38 -0
- package/README.md +1121 -2
- package/assets/branding/README.md +62 -0
- package/assets/branding/jackherd-icon-256.png +0 -0
- package/assets/branding/jackherd-icon-512.png +0 -0
- package/assets/branding/jackherd-icon-64.png +0 -0
- package/assets/branding/jackherd-icon-small-32.png +0 -0
- package/assets/branding/jackherd-icon-small-64.png +0 -0
- package/assets/branding/jackherd-icon-small.svg +1 -0
- package/assets/branding/jackherd-icon.svg +1 -0
- package/assets/branding/jackherd-logo-1024.png +0 -0
- package/assets/branding/jackherd-logo-512.png +0 -0
- package/assets/branding/jackherd-logo-dark-1024.png +0 -0
- package/assets/branding/jackherd-logo-dark-512.png +0 -0
- package/assets/branding/jackherd-logo-dark.svg +1 -0
- package/assets/branding/jackherd-logo-mark-1024.png +0 -0
- package/assets/branding/jackherd-logo-mark-512.png +0 -0
- package/assets/branding/jackherd-logo-mark.svg +1 -0
- package/assets/branding/jackherd-logo.svg +1 -0
- package/dist/chunk-R6MGHCD7.js +93 -0
- package/dist/config.d.ts +167 -0
- package/dist/config.js +401 -0
- package/dist/plan-format.d.ts +325 -0
- package/dist/plan-format.js +636 -0
- package/docs/lanes.mmd +9 -0
- package/docs/lanes.svg +1 -0
- package/docs/skills.mmd +10 -0
- package/docs/skills.svg +1 -0
- package/docs/workflow.svg +172 -0
- package/herdr-plugin/herdr-plugin.toml +22 -0
- package/herdr-plugin/open-plans.ts +43 -0
- package/herdr-plugin/restore-plans.ts +107 -0
- package/install.sh +157 -0
- package/kit/hooks/hooks/git-guard/decision.ts +438 -0
- package/kit/hooks/hooks/git-guard/index.ts +38 -0
- package/kit/hooks/hooks/git-guard/shell.ts +407 -0
- package/kit/hooks/hooks/watch-guard/background.ts +36 -0
- package/kit/hooks/hooks/watch-guard/decision.ts +53 -0
- package/kit/hooks/hooks/watch-guard/index.ts +11 -0
- package/kit/hooks/hooks/watch-guard/shell-input.ts +32 -0
- package/kit/hooks/hooks/watch-guard/starts.ts +154 -0
- package/kit/hooks/hooks/watch-guard/text-fallback.ts +40 -0
- package/kit/hooks/hooks.json +30 -0
- package/kit/hooks/statusline.sh +72 -0
- package/kit/review/agents/devops.md +11 -0
- package/kit/review/agents/public-content.md +39 -0
- package/kit/review/agents/security-audit.md +11 -0
- package/kit/review/agents/uncle-bob.md +11 -0
- package/kit/review/skills/devops/SKILL.md +50 -0
- package/kit/review/skills/diagnose/SKILL.md +140 -0
- package/kit/review/skills/diagnose/references/feedback-loops.md +59 -0
- package/kit/review/skills/diagnose/scripts/hitl-loop.template.sh +71 -0
- package/kit/review/skills/pre-commit/SKILL.md +100 -0
- package/kit/review/skills/review/SKILL.md +43 -0
- package/kit/review/skills/security-audit/SKILL.md +65 -0
- package/kit/review/skills/uncle-bob/SKILL.md +43 -0
- package/kit/rules/rules/kit/arc42.md +89 -0
- package/kit/rules/rules/kit/architecture-files.md +87 -0
- package/kit/rules/rules/kit/code-style.md +159 -0
- package/kit/rules/rules/kit/pull-requests.md +59 -0
- package/kit/rules/rules/kit/testing.md +91 -0
- package/kit/rules/rules/kit/writing-for-agents.md +110 -0
- package/kit/workflow/agents/explore.md +25 -0
- package/kit/workflow/agents/implement.md +16 -0
- package/kit/workflow/agents/plan.md +13 -0
- package/kit/workflow/agents/spec-review.md +96 -0
- package/kit/workflow/skills/follow/SKILL.md +48 -0
- package/kit/workflow/skills/grill/SKILL.md +60 -0
- package/kit/workflow/skills/implement/SKILL.md +107 -0
- package/kit/workflow/skills/orchestrate/SKILL.md +565 -0
- package/kit/workflow/skills/orchestrate/handover/decision.ts +247 -0
- package/kit/workflow/skills/orchestrate/handover/index.ts +171 -0
- package/kit/workflow/skills/plan/SKILL.md +118 -0
- package/kit/workflow/skills/plan/references/template-English.md +92 -0
- package/kit/workflow/skills/plan/references/template-German.md +92 -0
- package/kit/workflow/skills/start/SKILL.md +47 -0
- package/package.json +63 -5
- package/schemas/jackherd.schema.json +601 -0
- package/src/check/changes.ts +45 -0
- package/src/check/command-line.ts +12 -0
- package/src/check/command.ts +136 -0
- package/src/check/deferral-arrows.ts +41 -0
- package/src/check/execute.ts +29 -0
- package/src/check/glob.ts +15 -0
- package/src/check/plan-structure-check.ts +135 -0
- package/src/check/plan.ts +106 -0
- package/src/check/public-content-check.ts +32 -0
- package/src/check/public-content-facts.ts +177 -0
- package/src/check/public-content.ts +216 -0
- package/src/check/report.ts +55 -0
- package/src/check/result.ts +42 -0
- package/src/check/roots.ts +36 -0
- package/src/check/routing.ts +22 -0
- package/src/check/schema.ts +83 -0
- package/src/check/settings.ts +44 -0
- package/src/claude/paths.ts +14 -0
- package/src/cli.ts +46 -0
- package/src/config/command.ts +45 -0
- package/src/config/detect.ts +196 -0
- package/src/config/index.ts +2 -0
- package/src/config/json-schema.ts +5 -0
- package/src/config/legacy-names.ts +56 -0
- package/src/config/link-kinds.ts +25 -0
- package/src/config/location.ts +46 -0
- package/src/config/read-config.ts +42 -0
- package/src/config/repository-facts.ts +19 -0
- package/src/config/repository-values.ts +46 -0
- package/src/config/schema.ts +302 -0
- package/src/context/command.ts +39 -0
- package/src/context/coordinator-name.ts +16 -0
- package/src/context/fill.ts +69 -0
- package/src/context/index.ts +10 -0
- package/src/context/level.ts +22 -0
- package/src/context/options.ts +29 -0
- package/src/context/report.ts +42 -0
- package/src/context/session-log.ts +91 -0
- package/src/plan-format/drift.ts +69 -0
- package/src/plan-format/header.ts +27 -0
- package/src/plan-format/index.ts +9 -0
- package/src/plan-format/lanes.ts +128 -0
- package/src/plan-format/markdown.ts +98 -0
- package/src/plan-format/phase-tally.ts +55 -0
- package/src/plan-format/phases.ts +224 -0
- package/src/plan-format/plan-structure.ts +150 -0
- package/src/plan-format/progress.ts +202 -0
- package/src/plan-format/sprint-document.ts +69 -0
- package/src/plan-format/terminal-text.ts +28 -0
- package/src/plans/board-choice.ts +60 -0
- package/src/plans/board-pick.ts +72 -0
- package/src/plans/board-rows.ts +62 -0
- package/src/plans/command.ts +60 -0
- package/src/plans/ensure-panes.ts +71 -0
- package/src/plans/error-text.ts +5 -0
- package/src/plans/fetch.ts +96 -0
- package/src/plans/git.ts +145 -0
- package/src/plans/github.ts +35 -0
- package/src/plans/lane-discovery.ts +34 -0
- package/src/plans/lane-plans.ts +80 -0
- package/src/plans/layout.ts +881 -0
- package/src/plans/link-chips.ts +74 -0
- package/src/plans/live.ts +336 -0
- package/src/plans/markdown-html.ts +140 -0
- package/src/plans/options.ts +15 -0
- package/src/plans/phase-source.ts +71 -0
- package/src/plans/plan-chips.ts +75 -0
- package/src/plans/plan-panes.ts +206 -0
- package/src/plans/planned.ts +117 -0
- package/src/plans/pull-requests.ts +107 -0
- package/src/plans/restart-decision.ts +42 -0
- package/src/plans/selection.ts +352 -0
- package/src/plans/settings.ts +68 -0
- package/src/plans/single-flight.ts +25 -0
- package/src/plans/snapshot.ts +300 -0
- package/src/plans/sprint-export.ts +103 -0
- package/src/plans/sprint-html.ts +301 -0
- package/src/plans/sprint-page.ts +287 -0
- package/src/plans/supervisor.ts +124 -0
- package/src/plans/symbols.ts +54 -0
- package/src/plans/texts.ts +103 -0
- package/src/plans/unlisted-lanes.ts +45 -0
- package/src/plans/view.ts +68 -0
- package/src/plans/worked-phases.ts +53 -0
- package/src/sync/checksum.ts +13 -0
- package/src/sync/choice.ts +14 -0
- package/src/sync/command-context.ts +113 -0
- package/src/sync/command.ts +94 -0
- package/src/sync/error-message.ts +1 -0
- package/src/sync/gitignore.ts +45 -0
- package/src/sync/global-kit.ts +176 -0
- package/src/sync/herdr-plugin.ts +175 -0
- package/src/sync/if-changed.ts +109 -0
- package/src/sync/init-command.ts +239 -0
- package/src/sync/init.ts +136 -0
- package/src/sync/kit-hooks.ts +18 -0
- package/src/sync/kit-path.ts +77 -0
- package/src/sync/kit-source.ts +169 -0
- package/src/sync/kit.ts +9 -0
- package/src/sync/legacy-files.ts +116 -0
- package/src/sync/manifest.ts +44 -0
- package/src/sync/next-steps.ts +42 -0
- package/src/sync/plan-file.ts +154 -0
- package/src/sync/plan-hooks.ts +165 -0
- package/src/sync/project-fs.ts +132 -0
- package/src/sync/render.ts +60 -0
- package/src/sync/report.ts +96 -0
- package/src/sync/settings-hooks.ts +90 -0
- package/src/sync/settings-migration.ts +94 -0
- package/src/sync/settings-offer.ts +168 -0
- package/src/sync/setup-questions.ts +70 -0
- package/src/sync/setup.ts +52 -0
- package/src/sync/sync-plan.ts +120 -0
- package/src/sync/sync-result.ts +9 -0
- package/src/sync/sync.ts +307 -0
- package/src/sync/tools.ts +52 -0
- package/src/terminal/herdr.ts +91 -0
- package/src/terminal/index.ts +13 -0
- package/src/terminal/none.ts +7 -0
- package/src/terminal/open.ts +39 -0
- package/src/terminal/text.ts +159 -0
- package/src/terminal/types.ts +30 -0
- package/src/terminal/width.ts +90 -0
- package/src/update/command.ts +65 -0
- package/src/update/package-manager.ts +56 -0
- package/src/update/update.ts +105 -0
- package/src/version/installed-version.ts +32 -0
- package/src/version/release.ts +22 -0
- package/src/version/skill-diff.ts +63 -0
- package/src/version/started-kit.ts +14 -0
- package/src/vocabulary/index.ts +132 -0
- package/src/watch/base-tip.ts +18 -0
- package/src/watch/command.ts +919 -0
- package/src/watch/coordinator-fill.ts +76 -0
- package/src/watch/feedback-read.ts +73 -0
- package/src/watch/feedback.ts +308 -0
- package/src/watch/gh.ts +22 -0
- package/src/watch/github-read.ts +175 -0
- package/src/watch/herdr.ts +11 -0
- package/src/watch/json-guards.ts +9 -0
- package/src/watch/kit-update.ts +18 -0
- package/src/watch/lane-fill.ts +36 -0
- package/src/watch/lane-message.ts +99 -0
- package/src/watch/lane-progress.ts +44 -0
- package/src/watch/lane-report-file.ts +53 -0
- package/src/watch/lane-report.ts +270 -0
- package/src/watch/lane-search.ts +41 -0
- package/src/watch/lane-sources.ts +46 -0
- package/src/watch/lane-watch.ts +121 -0
- package/src/watch/monitor-tool.ts +19 -0
- package/src/watch/options.ts +79 -0
- package/src/watch/plan-badges-read.ts +30 -0
- package/src/watch/plan-badges.ts +66 -0
- package/src/watch/plan-views.ts +59 -0
- package/src/watch/preview-read.ts +60 -0
- package/src/watch/previews.ts +234 -0
- package/src/watch/pull-requests.ts +117 -0
- package/src/watch/single-report.ts +15 -0
- package/src/watch/skill-change.ts +61 -0
- package/src/watch/source-dry-run-command.ts +35 -0
- package/src/watch/source-dry-run.ts +124 -0
- package/src/watch/source-fields.ts +9 -0
- package/src/watch/source-path.ts +40 -0
- package/src/watch/source-run.ts +90 -0
- package/src/watch/sources.ts +344 -0
- package/src/watch/stream-output.ts +14 -0
- package/src/watch/texts.ts +76 -0
- package/src/watch/watch-events.ts +399 -0
- package/src/watch/watcher-lock.ts +145 -0
- package/src/watch/workflow-runs.ts +96 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/** The herdr agent in the coordinator's pane, as one `herdr agent list` entry shows it. */
|
|
2
|
+
export type PaneAgent = { status: string; sessionId: string | null; name: string | null; seq: number }
|
|
3
|
+
|
|
4
|
+
/** One look at the pane: its agent, no agent at all, or a listing that could not be read. */
|
|
5
|
+
export type PaneReading =
|
|
6
|
+
| { kind: 'agent'; agent: PaneAgent }
|
|
7
|
+
| { kind: 'none' }
|
|
8
|
+
| { kind: 'unreadable'; reason: string }
|
|
9
|
+
|
|
10
|
+
/** The herdr agent states the hand-over acts on; herdr knows more (`unknown`), which count as not ready. */
|
|
11
|
+
const AGENT_STATUS = { idle: 'idle', done: 'done', working: 'working', blocked: 'blocked' } as const
|
|
12
|
+
|
|
13
|
+
export const HANDOVER_PHASE = {
|
|
14
|
+
waitIdle: 'waitIdle',
|
|
15
|
+
waitSession: 'waitSession',
|
|
16
|
+
waitWork: 'waitWork',
|
|
17
|
+
finished: 'finished',
|
|
18
|
+
aborted: 'aborted',
|
|
19
|
+
} as const
|
|
20
|
+
|
|
21
|
+
/** Since when the agent has been ready without a new `state_change_seq`. */
|
|
22
|
+
type Quiet = { since: number; seq: number }
|
|
23
|
+
|
|
24
|
+
type WaitIdle = {
|
|
25
|
+
phase: typeof HANDOVER_PHASE.waitIdle
|
|
26
|
+
since: number
|
|
27
|
+
sessionId: string
|
|
28
|
+
name: string | null
|
|
29
|
+
quiet: Quiet | null
|
|
30
|
+
}
|
|
31
|
+
type WaitSession = { phase: typeof HANDOVER_PHASE.waitSession; since: number; sessionId: string; name: string | null }
|
|
32
|
+
type WaitWork = { phase: typeof HANDOVER_PHASE.waitWork; since: number; sessionId: string }
|
|
33
|
+
type Waiting = WaitIdle | WaitSession | WaitWork
|
|
34
|
+
|
|
35
|
+
export type HandoverState =
|
|
36
|
+
| Waiting
|
|
37
|
+
| { phase: typeof HANDOVER_PHASE.finished }
|
|
38
|
+
| { phase: typeof HANDOVER_PHASE.aborted; reason: string }
|
|
39
|
+
|
|
40
|
+
/** `send` restores the herdr name first when `rename` carries one. */
|
|
41
|
+
type HandoverAction =
|
|
42
|
+
| { kind: 'wait' }
|
|
43
|
+
| { kind: 'clear' }
|
|
44
|
+
| { kind: 'send'; rename: string | null }
|
|
45
|
+
| { kind: 'finish' }
|
|
46
|
+
| { kind: 'abort'; reason: string }
|
|
47
|
+
|
|
48
|
+
type HandoverStep = { state: HandoverState; action: HandoverAction }
|
|
49
|
+
|
|
50
|
+
export type HandoverOptions = { pane: string; send: string; log: string; foreground: boolean }
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `settleMs`: how long the agent stays ready with the same `state_change_seq` before `/clear`, so
|
|
54
|
+
* the turn that started the script has really ended. The timeouts bound each wait in milliseconds.
|
|
55
|
+
*/
|
|
56
|
+
export const HANDOVER_TIMING = {
|
|
57
|
+
pollMs: 1000,
|
|
58
|
+
settleMs: 3000,
|
|
59
|
+
idleTimeoutMs: 15 * 60_000,
|
|
60
|
+
clearTimeoutMs: 60_000,
|
|
61
|
+
workTimeoutMs: 60_000,
|
|
62
|
+
} as const
|
|
63
|
+
|
|
64
|
+
const DEFAULT_HANDOVER_LOG = '.claude/handover/coordinator.log'
|
|
65
|
+
|
|
66
|
+
const READY_STATUS = new Set<string>([AGENT_STATUS.idle, AGENT_STATUS.done])
|
|
67
|
+
const STARTED_STATUS = new Set<string>([AGENT_STATUS.working, AGENT_STATUS.blocked])
|
|
68
|
+
|
|
69
|
+
const PHASE_TIMEOUT_MS: Record<Waiting['phase'], number> = {
|
|
70
|
+
waitIdle: HANDOVER_TIMING.idleTimeoutMs,
|
|
71
|
+
waitSession: HANDOVER_TIMING.clearTimeoutMs,
|
|
72
|
+
waitWork: HANDOVER_TIMING.workTimeoutMs,
|
|
73
|
+
}
|
|
74
|
+
const NO_AGENT = 'no agent in the pane'
|
|
75
|
+
const UNEXPECTED_LISTING = 'unexpected herdr agent list'
|
|
76
|
+
|
|
77
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null
|
|
78
|
+
|
|
79
|
+
const stringOrNull = (value: unknown): string | null => (typeof value === 'string' && value !== '' ? value : null)
|
|
80
|
+
|
|
81
|
+
const toPaneAgent = (entry: Record<string, unknown>): PaneAgent | null => {
|
|
82
|
+
const { agent_status: status, state_change_seq: seq, agent_session: session, name } = entry
|
|
83
|
+
if (typeof status !== 'string' || typeof seq !== 'number') return null
|
|
84
|
+
return { status, sessionId: isRecord(session) ? stringOrNull(session.value) : null, name: stringOrNull(name), seq }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Finds the agent of one pane in the JSON of `herdr agent list`. */
|
|
88
|
+
export const paneAgentOf = (listing: unknown, pane: string): PaneReading => {
|
|
89
|
+
const agents = isRecord(listing) && isRecord(listing.result) ? listing.result.agents : undefined
|
|
90
|
+
if (!Array.isArray(agents)) return { kind: 'unreadable', reason: UNEXPECTED_LISTING }
|
|
91
|
+
const entry = agents.find((candidate) => isRecord(candidate) && candidate.pane_id === pane)
|
|
92
|
+
if (entry === undefined) return { kind: 'none' }
|
|
93
|
+
const agent = toPaneAgent(entry)
|
|
94
|
+
return agent ? { kind: 'agent', agent } : { kind: 'unreadable', reason: UNEXPECTED_LISTING }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const startHandover = (
|
|
98
|
+
reading: PaneReading,
|
|
99
|
+
now: number,
|
|
100
|
+
): { ok: true; state: HandoverState } | { ok: false; reason: string } => {
|
|
101
|
+
if (reading.kind === 'none') return { ok: false, reason: NO_AGENT }
|
|
102
|
+
if (reading.kind === 'unreadable') return { ok: false, reason: reading.reason }
|
|
103
|
+
const { sessionId, name } = reading.agent
|
|
104
|
+
if (sessionId === null) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
reason: 'the agent reports no session id, so a cleared chat could not be told from the old one',
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { ok: true, state: { phase: HANDOVER_PHASE.waitIdle, since: now, sessionId, name, quiet: null } }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const stay = (state: HandoverState): HandoverStep => ({ state, action: { kind: 'wait' } })
|
|
114
|
+
|
|
115
|
+
const abort = (reason: string): HandoverStep => ({
|
|
116
|
+
state: { phase: HANDOVER_PHASE.aborted, reason },
|
|
117
|
+
action: { kind: 'abort', reason },
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const minutes = (milliseconds: number): number => milliseconds / 60_000
|
|
121
|
+
const seconds = (milliseconds: number): number => milliseconds / 1000
|
|
122
|
+
|
|
123
|
+
/** The one place that decides whether a wait has run out, whatever the pane showed meanwhile. */
|
|
124
|
+
const isPastTimeout = (state: Waiting, now: number): boolean => now - state.since > PHASE_TIMEOUT_MS[state.phase]
|
|
125
|
+
|
|
126
|
+
const stepWaitIdle = (state: WaitIdle, agent: PaneAgent, now: number): HandoverStep => {
|
|
127
|
+
if (agent.sessionId !== null && agent.sessionId !== state.sessionId) {
|
|
128
|
+
return abort(`the session changed before /clear was sent (${state.sessionId} → ${agent.sessionId})`)
|
|
129
|
+
}
|
|
130
|
+
if (isPastTimeout(state, now)) {
|
|
131
|
+
return abort(`the agent was not idle within ${minutes(HANDOVER_TIMING.idleTimeoutMs)} min`)
|
|
132
|
+
}
|
|
133
|
+
if (agent.sessionId === null) return stay(state)
|
|
134
|
+
if (!READY_STATUS.has(agent.status)) return stay({ ...state, quiet: null })
|
|
135
|
+
if (state.quiet === null || state.quiet.seq !== agent.seq)
|
|
136
|
+
return stay({ ...state, quiet: { since: now, seq: agent.seq } })
|
|
137
|
+
if (now - state.quiet.since < HANDOVER_TIMING.settleMs) return stay(state)
|
|
138
|
+
return {
|
|
139
|
+
state: { phase: HANDOVER_PHASE.waitSession, since: now, sessionId: state.sessionId, name: state.name },
|
|
140
|
+
action: { kind: 'clear' },
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const sessionTimeoutReason = (state: WaitSession, sessionId: string | null): string => {
|
|
145
|
+
const limit = seconds(HANDOVER_TIMING.clearTimeoutMs)
|
|
146
|
+
if (sessionId === null) return `the agent reported no session ${limit} s after /clear`
|
|
147
|
+
if (sessionId === state.sessionId)
|
|
148
|
+
return `the session is still ${state.sessionId} ${limit} s after /clear: the chat was not cleared`
|
|
149
|
+
return `the new session ${sessionId} was not ready within ${limit} s`
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const stepWaitSession = (state: WaitSession, agent: PaneAgent, now: number): HandoverStep => {
|
|
153
|
+
const { sessionId } = agent
|
|
154
|
+
if (sessionId !== null && sessionId !== state.sessionId && READY_STATUS.has(agent.status)) {
|
|
155
|
+
const rename = state.name !== null && agent.name !== state.name ? state.name : null
|
|
156
|
+
return { state: { phase: HANDOVER_PHASE.waitWork, since: now, sessionId }, action: { kind: 'send', rename } }
|
|
157
|
+
}
|
|
158
|
+
return isPastTimeout(state, now) ? abort(sessionTimeoutReason(state, sessionId)) : stay(state)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const stepWaitWork = (state: WaitWork, agent: PaneAgent, now: number): HandoverStep => {
|
|
162
|
+
if (agent.sessionId !== null && agent.sessionId !== state.sessionId) {
|
|
163
|
+
return abort(`the session changed again after the command (${state.sessionId} → ${agent.sessionId})`)
|
|
164
|
+
}
|
|
165
|
+
if (agent.sessionId !== null && STARTED_STATUS.has(agent.status)) {
|
|
166
|
+
return { state: { phase: HANDOVER_PHASE.finished }, action: { kind: 'finish' } }
|
|
167
|
+
}
|
|
168
|
+
if (isPastTimeout(state, now)) {
|
|
169
|
+
return abort(`the agent did not start working within ${seconds(HANDOVER_TIMING.workTimeoutMs)} s of the command`)
|
|
170
|
+
}
|
|
171
|
+
return stay(state)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* One poll of the hand-over: wait until the agent settled, clear, wait for a new session, send the
|
|
176
|
+
* command, wait for its turn. An unreadable listing waits, but never past the phase's timeout.
|
|
177
|
+
*/
|
|
178
|
+
export const handoverStep = (state: HandoverState, reading: PaneReading, now: number): HandoverStep => {
|
|
179
|
+
if (state.phase === HANDOVER_PHASE.finished || state.phase === HANDOVER_PHASE.aborted) return stay(state)
|
|
180
|
+
if (reading.kind === 'none') return abort(NO_AGENT)
|
|
181
|
+
if (reading.kind === 'unreadable') {
|
|
182
|
+
return isPastTimeout(state, now)
|
|
183
|
+
? abort(`herdr could not be read (${reading.reason}) until the ${state.phase} timeout ran out`)
|
|
184
|
+
: stay(state)
|
|
185
|
+
}
|
|
186
|
+
if (state.phase === HANDOVER_PHASE.waitIdle) return stepWaitIdle(state, reading.agent, now)
|
|
187
|
+
if (state.phase === HANDOVER_PHASE.waitSession) return stepWaitSession(state, reading.agent, now)
|
|
188
|
+
return stepWaitWork(state, reading.agent, now)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** One line for the log: what the pane showed at a step. */
|
|
192
|
+
export const describeReading = (reading: PaneReading): string => {
|
|
193
|
+
switch (reading.kind) {
|
|
194
|
+
case 'agent': {
|
|
195
|
+
const { status, sessionId, name } = reading.agent
|
|
196
|
+
return `status ${status}, session ${sessionId ?? '(none)'}, name ${name ?? '(none)'}`
|
|
197
|
+
}
|
|
198
|
+
case 'none':
|
|
199
|
+
return 'no agent'
|
|
200
|
+
case 'unreadable':
|
|
201
|
+
return `unreadable (${reading.reason})`
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export const manualPath = (pane: string, line: string): string =>
|
|
206
|
+
`by hand: type /clear in pane ${pane}, then type: ${line}`
|
|
207
|
+
|
|
208
|
+
const VALUE_OPTIONS = new Set(['--send', '--pane', '--log'])
|
|
209
|
+
|
|
210
|
+
const readArguments = (argv: string[]): { values: Map<string, string>; foreground: boolean } | { error: string } => {
|
|
211
|
+
const values = new Map<string, string>()
|
|
212
|
+
let foreground = false
|
|
213
|
+
for (let index = 0; index < argv.length; index++) {
|
|
214
|
+
const option = argv[index]
|
|
215
|
+
if (option === '--foreground') {
|
|
216
|
+
foreground = true
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
if (!VALUE_OPTIONS.has(option)) return { error: `unknown option ${option}` }
|
|
220
|
+
const value = argv[index + 1]
|
|
221
|
+
if (value === undefined) return { error: `${option} needs a value` }
|
|
222
|
+
values.set(option, value)
|
|
223
|
+
index++
|
|
224
|
+
}
|
|
225
|
+
return { values, foreground }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* `--send <line>` is the one input typed into the cleared chat. `--foreground` runs the hand-over
|
|
230
|
+
* in this process; without it the script detaches a copy of itself and returns.
|
|
231
|
+
*/
|
|
232
|
+
export const parseHandoverOptions = (
|
|
233
|
+
argv: string[],
|
|
234
|
+
env: Record<string, string | undefined>,
|
|
235
|
+
): { ok: true; options: HandoverOptions } | { ok: false; error: string } => {
|
|
236
|
+
const parsed = readArguments(argv)
|
|
237
|
+
if ('error' in parsed) return { ok: false, error: parsed.error }
|
|
238
|
+
const send = parsed.values.get('--send')?.trim() ?? ''
|
|
239
|
+
if (send === '') return { ok: false, error: '--send <line> is required' }
|
|
240
|
+
if (/[\r\n]/.test(send)) {
|
|
241
|
+
return { ok: false, error: '--send must be one line: a line break would submit the rest as a second input' }
|
|
242
|
+
}
|
|
243
|
+
const pane = parsed.values.get('--pane') ?? env.HERDR_PANE_ID
|
|
244
|
+
if (!pane) return { ok: false, error: 'no pane: run inside herdr (HERDR_PANE_ID) or pass --pane <id>' }
|
|
245
|
+
const log = parsed.values.get('--log') ?? DEFAULT_HANDOVER_LOG
|
|
246
|
+
return { ok: true, options: { pane, send, log, foreground: parsed.foreground } }
|
|
247
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Hands the coordinator over to a fresh chat, shipped by jackherd. Run from the
|
|
2
|
+
// coordinator's own Bash call: it detaches a copy of itself and returns, the copy waits until the
|
|
3
|
+
// coordinator's pane is idle, sends /clear, checks that a new session runs and then types the
|
|
4
|
+
// command. Every step goes to the log; an abort also names the way by hand and shows a herdr notice.
|
|
5
|
+
import { spawn } from 'node:child_process'
|
|
6
|
+
import { appendFile, mkdir } from 'node:fs/promises'
|
|
7
|
+
import { dirname, resolve } from 'node:path'
|
|
8
|
+
import {
|
|
9
|
+
describeReading,
|
|
10
|
+
HANDOVER_TIMING,
|
|
11
|
+
type HandoverOptions,
|
|
12
|
+
type HandoverState,
|
|
13
|
+
handoverStep,
|
|
14
|
+
manualPath,
|
|
15
|
+
type PaneReading,
|
|
16
|
+
paneAgentOf,
|
|
17
|
+
parseHandoverOptions,
|
|
18
|
+
startHandover,
|
|
19
|
+
} from './decision'
|
|
20
|
+
|
|
21
|
+
type HerdrResult = { ok: true; stdout: string } | { ok: false; error: string }
|
|
22
|
+
|
|
23
|
+
const EXIT = { done: 0, aborted: 1, usage: 2 } as const
|
|
24
|
+
|
|
25
|
+
const ABORT_TITLE = 'Hand-over aborted'
|
|
26
|
+
|
|
27
|
+
const herdr = async (args: string[]): Promise<HerdrResult> => {
|
|
28
|
+
try {
|
|
29
|
+
const child = Bun.spawn(['herdr', ...args], { stdout: 'pipe', stderr: 'pipe' })
|
|
30
|
+
const [stdout, stderr, code] = await Promise.all([
|
|
31
|
+
new Response(child.stdout).text(),
|
|
32
|
+
new Response(child.stderr).text(),
|
|
33
|
+
child.exited,
|
|
34
|
+
])
|
|
35
|
+
return code === 0 ? { ok: true, stdout } : { ok: false, error: (stderr || stdout).trim() || `exit ${code}` }
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const readPane = async (pane: string): Promise<PaneReading> => {
|
|
42
|
+
const listed = await herdr(['agent', 'list'])
|
|
43
|
+
if (!listed.ok) return { kind: 'unreadable', reason: `herdr agent list: ${listed.error}` }
|
|
44
|
+
try {
|
|
45
|
+
return paneAgentOf(JSON.parse(listed.stdout), pane)
|
|
46
|
+
} catch {
|
|
47
|
+
return { kind: 'unreadable', reason: 'herdr agent list answered no JSON' }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const writeLog = async (path: string, line: string): Promise<void> => {
|
|
52
|
+
const time = new Date().toTimeString().slice(0, 8)
|
|
53
|
+
await appendFile(path, `${time} ${line}\n`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const sleep = (milliseconds: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, milliseconds))
|
|
57
|
+
|
|
58
|
+
type Log = (line: string) => Promise<void>
|
|
59
|
+
|
|
60
|
+
/** Returns the reason of a failed herdr call, or null once `/clear` went out. */
|
|
61
|
+
const sendClear = async (options: HandoverOptions, log: Log): Promise<string | null> => {
|
|
62
|
+
const cleared = await herdr(['agent', 'prompt', options.pane, '/clear'])
|
|
63
|
+
if (!cleared.ok) return `herdr agent prompt /clear failed: ${cleared.error}`
|
|
64
|
+
await log('sent /clear')
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A failed rename is only logged: the name is a convenience, the line is the hand-over. */
|
|
69
|
+
const sendCommand = async (rename: string | null, options: HandoverOptions, log: Log): Promise<string | null> => {
|
|
70
|
+
if (rename !== null) {
|
|
71
|
+
const renamed = await herdr(['agent', 'rename', options.pane, rename])
|
|
72
|
+
await log(renamed.ok ? `renamed the agent to ${rename}` : `rename to ${rename} failed: ${renamed.error}`)
|
|
73
|
+
}
|
|
74
|
+
const sent = await herdr(['agent', 'prompt', options.pane, options.send])
|
|
75
|
+
if (!sent.ok) return `herdr agent prompt failed: ${sent.error}`
|
|
76
|
+
await log(`sent: ${options.send}`)
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const showAbortNotice = (reason: string, options: HandoverOptions): Promise<HerdrResult> =>
|
|
81
|
+
herdr([
|
|
82
|
+
'notification',
|
|
83
|
+
'show',
|
|
84
|
+
ABORT_TITLE,
|
|
85
|
+
'--body',
|
|
86
|
+
`${reason}. ${manualPath(options.pane, options.send)}`,
|
|
87
|
+
'--sound',
|
|
88
|
+
'request',
|
|
89
|
+
])
|
|
90
|
+
|
|
91
|
+
/** Logs the abort and tells the user through a herdr notice: the coordinator's turn has ended, nobody else would. */
|
|
92
|
+
const finishAborted = async (reason: string, options: HandoverOptions, log: Log): Promise<number> => {
|
|
93
|
+
await log(`aborted: ${reason}`)
|
|
94
|
+
await log(manualPath(options.pane, options.send))
|
|
95
|
+
const notice = await showAbortNotice(reason, options)
|
|
96
|
+
if (!notice.ok) await log(`notice failed: ${notice.error}`)
|
|
97
|
+
return EXIT.aborted
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const runHandover = async (options: HandoverOptions): Promise<number> => {
|
|
101
|
+
await mkdir(dirname(options.log), { recursive: true })
|
|
102
|
+
const log: Log = (line) => writeLog(options.log, line)
|
|
103
|
+
const initialReading = await readPane(options.pane)
|
|
104
|
+
const started = startHandover(initialReading, Date.now())
|
|
105
|
+
if (!started.ok) return finishAborted(started.reason, options, log)
|
|
106
|
+
await log(`pid ${process.pid}: waiting for pane ${options.pane} to settle (${describeReading(initialReading)})`)
|
|
107
|
+
let state: HandoverState = started.state
|
|
108
|
+
while (true) {
|
|
109
|
+
await sleep(HANDOVER_TIMING.pollMs)
|
|
110
|
+
const reading = await readPane(options.pane)
|
|
111
|
+
const { state: next, action } = handoverStep(state, reading, Date.now())
|
|
112
|
+
if (next.phase !== state.phase) await log(`${state.phase} → ${next.phase}: ${describeReading(reading)}`)
|
|
113
|
+
state = next
|
|
114
|
+
let failure: string | null = null
|
|
115
|
+
switch (action.kind) {
|
|
116
|
+
case 'finish':
|
|
117
|
+
await log('delivered: the new session is working on the command')
|
|
118
|
+
return EXIT.done
|
|
119
|
+
case 'abort':
|
|
120
|
+
return finishAborted(action.reason, options, log)
|
|
121
|
+
case 'clear':
|
|
122
|
+
failure = await sendClear(options, log)
|
|
123
|
+
break
|
|
124
|
+
case 'send':
|
|
125
|
+
failure = await sendCommand(action.rename, options, log)
|
|
126
|
+
break
|
|
127
|
+
}
|
|
128
|
+
if (failure !== null) return finishAborted(failure, options, log)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The detached copy has no terminal: a crash (a log it cannot write) must still reach the user. */
|
|
133
|
+
const runVisibly = async (options: HandoverOptions): Promise<number> => {
|
|
134
|
+
try {
|
|
135
|
+
return await runHandover(options)
|
|
136
|
+
} catch (error) {
|
|
137
|
+
await showAbortNotice(
|
|
138
|
+
`the hand-over script failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
139
|
+
options,
|
|
140
|
+
)
|
|
141
|
+
return EXIT.aborted
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Starts this script again in a session of its own, so it outlives the Bash call and the /clear. */
|
|
146
|
+
const detach = async (argv: string[], options: HandoverOptions): Promise<number> => {
|
|
147
|
+
const started = startHandover(await readPane(options.pane), Date.now())
|
|
148
|
+
if (!started.ok) {
|
|
149
|
+
console.error(`hand-over not started: ${started.reason}`)
|
|
150
|
+
return EXIT.aborted
|
|
151
|
+
}
|
|
152
|
+
// node:child_process, not Bun.spawn: only it offers `detached`, a session of the child's own.
|
|
153
|
+
const child = spawn(process.execPath, [import.meta.path, ...argv, '--foreground'], {
|
|
154
|
+
detached: true,
|
|
155
|
+
stdio: 'ignore',
|
|
156
|
+
cwd: process.cwd(),
|
|
157
|
+
env: process.env,
|
|
158
|
+
})
|
|
159
|
+
child.unref()
|
|
160
|
+
console.log(`hand-over started: pid ${child.pid}, log ${options.log}`)
|
|
161
|
+
return EXIT.done
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const argv = process.argv.slice(2)
|
|
165
|
+
const parsed = parseHandoverOptions(argv, process.env)
|
|
166
|
+
if (!parsed.ok) {
|
|
167
|
+
console.error(parsed.error)
|
|
168
|
+
process.exit(EXIT.usage)
|
|
169
|
+
}
|
|
170
|
+
const options = { ...parsed.options, log: resolve(parsed.options.log) }
|
|
171
|
+
process.exit(options.foreground ? await runVisibly(options) : await detach(argv, options))
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plan
|
|
3
|
+
description: 'Create structured concept/plan documents for {{kit.planDirectory}}/. Use when planning new features or architectural changes.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
If `.claude/project/plan.md` exists, read it first. It extends this skill for this project (where to look in the pre-scan, which documents carry product context, extra review-gate items) and wins where the two disagree.
|
|
7
|
+
|
|
8
|
+
You create a structured concept/plan document in `{{kit.planDirectory}}/`.
|
|
9
|
+
|
|
10
|
+
> **Output language: {{kit.planLanguage}}.** These instructions are English; the plan document is
|
|
11
|
+
> written in {{kit.planLanguage}}. Keep the section headings of the template **verbatim** — the plan
|
|
12
|
+
> parser, `/implement` and `spec-review` find `## {{kit.heading.decisions}}`,
|
|
13
|
+
> `## {{kit.heading.implementation}}` and `## {{kit.heading.documentation}}` by those exact names.
|
|
14
|
+
|
|
15
|
+
## Your Workflow
|
|
16
|
+
|
|
17
|
+
### 0. Are the decisions actually made?
|
|
18
|
+
|
|
19
|
+
A plan records decisions; it is not where they get taken. If the `## {{kit.heading.decisions}}` table would end up holding your guesses rather than the user's answers, **stop and recommend `/grill`** — name the open questions you would otherwise have to invent answers for, and let the user decide whether to run it. Entering clarification mode is the user's call, so you recommend it and leave the invocation to them. It works the questions in rounds and hands back rows in this table's format.
|
|
20
|
+
|
|
21
|
+
Skip this when the request already carries its decisions (a plan for a bug fix with one obvious approach, a change the user has already specified).
|
|
22
|
+
|
|
23
|
+
### 1. Pre-Scan (before a single word of the plan is written)
|
|
24
|
+
|
|
25
|
+
- [ ] Read the relevant files in their **current** state, from disk
|
|
26
|
+
- [ ] Search the project's shared components and utilities for what covers 80%+ of the need. 80%+ match → the plan specifies reuse or an override; 60–80% → the plan extends the shared piece; below 60% → new code, with the rationale under the rejected alternatives
|
|
27
|
+
- [ ] Read related plans in `{{kit.planDirectory}}/` — check dependencies and open TODOs
|
|
28
|
+
- [ ] Read the architecture documentation the change touches — understand what is already documented before you plan
|
|
29
|
+
|
|
30
|
+
### 2. Create the Document
|
|
31
|
+
|
|
32
|
+
**Filename:** `{{kit.planDirectory}}/YYYY-MM-DD-<kebab-case-title>.md`
|
|
33
|
+
|
|
34
|
+
**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.
|
|
35
|
+
|
|
36
|
+
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 plans view, which shows it as "not in the plan".
|
|
37
|
+
|
|
38
|
+
A checkbox moved on purpose to a later phase stays where it is, unticked, and ends in the target: `- [ ] Update the documentation → P9`. The plans view counts it neither as open nor as done in its own phase and lists it as open in phase 9, so a phase whose only open box was moved on reads as finished. The arrow counts only at the end of the line; `jh check` refuses an open box in a changed plan that carries it anywhere else (`- [ ] → P9 Update …`). Once a phase's tasks are ticked on the base branch and the change does not tick it further, every box still open in it, gate boxes included, is ticked or moved on this way: `jh check` warns on a changed plan that leaves one.
|
|
39
|
+
|
|
40
|
+
What the template's tables mean:
|
|
41
|
+
|
|
42
|
+
- **Model recommendations per phase** — one row per phase, judged by **error cost × complexity**. The project's default model for implementation (`{{kit.model.implement}}`) is the default of every row. A phase moves up to the planning model (`{{kit.model.plan}}`) when its rationale names a concrete error cost: a breakable invariant, possible data loss, security or tenant isolation, a migration without a way back, distributed state across systems. "Complex" or "many files" alone keeps the default. Review gates with `/security-audit` always get the strongest model.
|
|
43
|
+
- **Dependencies and parallelization** (from three phases) — `/implement` reads it and starts every phase whose dependencies are done and that shares no written path with another, at the same time.
|
|
44
|
+
- **{{kit.heading.dependsOn}}:** phases of this plan (numbers) or external gates (`User: mockups`). A phase is ready when everything here is done.
|
|
45
|
+
- **{{kit.heading.writes}}:** the directories or files the phase creates or changes, as narrow as possible. Shared files many phases touch (barrels, translation files, `package.json`, generated types, architecture docs) are listed explicitly, so the overlap is visible.
|
|
46
|
+
- **{{kit.heading.serialMarker}}:** `Migration`, `<collection> schema bump`, `Event <type>`, a generator run. Two phases with a migration never run at the same time; neither do two that bump the same schema.
|
|
47
|
+
|
|
48
|
+
### 3. Architecture Review
|
|
49
|
+
|
|
50
|
+
Apply `/uncle-bob` to the created plan. Additionally check:
|
|
51
|
+
|
|
52
|
+
- **Interfaces:** Are the boundaries between modules/services clear? Who consumes what?
|
|
53
|
+
- **Blast radius:** Which existing consumers are affected? Are tests needed in all consuming packages?
|
|
54
|
+
|
|
55
|
+
### 4. Anchor the Review Gate in the Plan
|
|
56
|
+
|
|
57
|
+
Every phase gets the review gate block from the template at its end. Add the reviewers the project supplement names for its areas (a UI review for UI phases, for instance). A phase with tasks that genuinely needs no gate (documentation only, a draft) says so and why in a line of its own, in the plan's language: `**No review gate:** <reason>` (`en`), `**Kein Review-Gate:** <Grund>` (`de`). `jh check` refuses a phase the change adds with tasks and neither line.
|
|
58
|
+
|
|
59
|
+
### 5. Quality Checks
|
|
60
|
+
|
|
61
|
+
- **Focus:** Does the plan solve a concrete problem? No "would be nice to have".
|
|
62
|
+
- **Scope:** Is the scope clearly bounded? No feature-creep risk?
|
|
63
|
+
- **Feasibility:** Can the phases be split into individual PRs/commits?
|
|
64
|
+
- **References:** Are related plans in `{{kit.planDirectory}}/` linked?
|
|
65
|
+
- **Current state is fact-based:** Read code, count files, verify architecture; every claim has a source.
|
|
66
|
+
- **Shared code checked:** For every planned component it is documented why it is built new instead of reused or extended.
|
|
67
|
+
- **Tests defined:** The tests section is filled in or carries an explicit rationale why none are needed (decision tree in `.claude/rules/kit/testing.md`).
|
|
68
|
+
- **Model recommendations defined:** `## {{kit.heading.implementation}}` starts with the model table, one row per phase, justified by error cost × complexity.
|
|
69
|
+
- **Parallelism defined (3+ phases):** the dependencies table follows it. Check it against the phase bodies: every migration, schema bump and new event in a phase appears as a serial marker; every shared file appears under {{kit.heading.writes}}. A dependency that exists only in your head lets `/implement` start two phases that collide.
|
|
70
|
+
- **arc42 check (after the plan exists):** apply the filter in `.claude/rules/kit/arc42.md` to the finished plan and **ask the user concretely** which sections should be extended. On "yes", add a concrete task to `## {{kit.heading.documentation}}`.
|
|
71
|
+
- **Architecture check (after the plan exists):** apply the filter in `.claude/rules/kit/architecture-files.md` — new ownership, a new invariant or an accepted risk each trigger a concrete question about the unit's `ARCHITECTURE.md`.
|
|
72
|
+
- **arc42 closing phase (3+ phases or a new service/package):** end the plan with the architecture documentation phase from the template. Add arc42 sections beyond it only when they deliver real value.
|
|
73
|
+
- **Documentation planned:** `## {{kit.heading.documentation}}` names concretely what will be created or updated after completion.
|
|
74
|
+
|
|
75
|
+
### 6. Propose the board rows
|
|
76
|
+
|
|
77
|
+
A **board** gathers the lanes one coordinator runs with `/orchestrate`: `{{kit.board.file}}` in a directory
|
|
78
|
+
matching `{{kit.planDirectory}}/{{kit.board.pattern}}` (this project calls it a "{{kit.board.term}}"). The plan
|
|
79
|
+
already holds what a lane row needs, so end by proposing the rows; `/orchestrate` then starts the lanes from them.
|
|
80
|
+
Skip this step for a plan of one phase that nobody runs as a lane, and say so.
|
|
81
|
+
|
|
82
|
+
Derive the rows from the dependencies table and the model table, nothing else:
|
|
83
|
+
|
|
84
|
+
- **Own lane:** a phase that can run beside the others: no path under {{kit.heading.writes}} in common with a phase
|
|
85
|
+
of another lane, and no serial marker that another lane carries at the same time.
|
|
86
|
+
- **Continuation:** a phase that shares a written path or a serial marker with an earlier phase, or depends on it,
|
|
87
|
+
is a `↳` row under that phase's lane, in the same worktree.
|
|
88
|
+
- **Starts after:** from the phase's {{kit.heading.dependsOn}} cell, as the board reads it (`P2 merged`, an external
|
|
89
|
+
gate such as `User: mockups`); a phase without one starts at once.
|
|
90
|
+
- **Model:** the full model ID from the model table (`{{kit.model.implement}}`, `{{kit.model.plan}}`), never an alias.
|
|
91
|
+
- **Plan:** a Markdown link to the plan, its path relative to the board's directory (`../<file>.md` for a plan
|
|
92
|
+
directly under `{{kit.planDirectory}}`).
|
|
93
|
+
Plans stay where they are; the row only links them.
|
|
94
|
+
- **Lane name:** short kebab case, unique on the board. `/orchestrate` names the worktree directory and the herdr
|
|
95
|
+
agent after it, so the worktree directory and the herdr agent carry the same name; a name that differs leaves the
|
|
96
|
+
watcher reporting the lane as `no agent`.
|
|
97
|
+
- **Port:** `–`; the coordinator sets it when the lane starts, unless the project supplement assigns ports here.
|
|
98
|
+
|
|
99
|
+
Show the rows as the table they become: `## {{kit.heading.lanes}}` with the columns lane, port, plan, phases, starts
|
|
100
|
+
after, model, in this order and headed in {{kit.planLanguage}} (the plans view reads them by position). Then ask
|
|
101
|
+
where they go:
|
|
102
|
+
|
|
103
|
+
- **The newest board**, by name: the newest `{{kit.planDirectory}}/{{kit.board.pattern}}/{{kit.board.file}}`. The
|
|
104
|
+
rows are appended to its `## {{kit.heading.lanes}}` table.
|
|
105
|
+
- **A new board.** Ask for its name and propose one: the year and month or the topic, in the pattern's shape. Check
|
|
106
|
+
that the directory name matches `{{kit.board.pattern}}` before writing; if it does not, say why and ask again.
|
|
107
|
+
Create `{{kit.planDirectory}}/<name>/{{kit.board.file}}` with a `#` title and the `## {{kit.heading.lanes}}`
|
|
108
|
+
table holding the proposed rows.
|
|
109
|
+
- **Not now:** the rows stay in your answer, for `/orchestrate` to write when it starts the lanes.
|
|
110
|
+
|
|
111
|
+
Nothing is written without that answer. The board is a file like the plan: it goes into the same commit, under the
|
|
112
|
+
same approval.
|
|
113
|
+
|
|
114
|
+
After writing the rows, name the next step in one line: `/orchestrate <board path>`, from a Claude Code session
|
|
115
|
+
inside herdr. A plan or board that is not on `origin/{{kit.baseBranch}}` yet needs no merge first: `/orchestrate`
|
|
116
|
+
asks to commit it on a documentation branch and starts the lanes from there.
|
|
117
|
+
|
|
118
|
+
Ask every question with a fixed set of answers through the `AskUserQuestion` tool, in {{kit.responseLanguage}}.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Title
|
|
2
|
+
|
|
3
|
+
**{{kit.heading.date}}:** YYYY-MM-DD
|
|
4
|
+
**{{kit.heading.status}}:** Draft / In Progress / Done / Abandoned
|
|
5
|
+
**{{kit.heading.scope}}:** `affected/paths`
|
|
6
|
+
|
|
7
|
+
## Problem / Context
|
|
8
|
+
|
|
9
|
+
What is the trigger? Why now? One paragraph.
|
|
10
|
+
|
|
11
|
+
## Current State
|
|
12
|
+
|
|
13
|
+
Fact-based: code analysis, file counts, current architecture. Tables preferred.
|
|
14
|
+
|
|
15
|
+
## Target State
|
|
16
|
+
|
|
17
|
+
What changes concretely? Files, modules, flows.
|
|
18
|
+
|
|
19
|
+
## {{kit.heading.decisions}}
|
|
20
|
+
|
|
21
|
+
| Decision | Rationale |
|
|
22
|
+
| -------- | --------- |
|
|
23
|
+
|
|
24
|
+
## {{kit.heading.implementation}}
|
|
25
|
+
|
|
26
|
+
### {{kit.heading.modelRecommendations}}
|
|
27
|
+
|
|
28
|
+
| {{kit.heading.phase}} | Model | Rationale |
|
|
29
|
+
| ----- | ----- | --------- |
|
|
30
|
+
| 1 — ... | {{kit.model.implement}} | Error cost × complexity |
|
|
31
|
+
|
|
32
|
+
> **Applying the models:** `/implement` applies this table on its own (step 0, model gate): it runs every phase whose recommendation differs from the running model through a subagent with the recommended model, in both directions. Whoever implements **without** `/implement` sets the model themselves at the start.
|
|
33
|
+
|
|
34
|
+
### {{kit.heading.dependencies}}
|
|
35
|
+
|
|
36
|
+
| {{kit.heading.phase}} | {{kit.heading.dependsOn}} | {{kit.heading.writes}} | {{kit.heading.serialMarker}} |
|
|
37
|
+
| ----- | ------------ | -------- | -------------- |
|
|
38
|
+
| 1 — ... | — | `path/to/module/` | Migration |
|
|
39
|
+
| 2 — ... | 1 | `other/module/` | — |
|
|
40
|
+
|
|
41
|
+
> **Applying the parallelism:** `/implement` starts ready phases without a shared written path and without colliding serial markers at the same time (step 0b), each with its model from the table above. Whoever implements without `/implement` reads here which sessions may run in parallel.
|
|
42
|
+
|
|
43
|
+
### {{kit.heading.phase}} 1: ...
|
|
44
|
+
|
|
45
|
+
- [ ] Step 1
|
|
46
|
+
- [ ] Step 2
|
|
47
|
+
|
|
48
|
+
**Review gate {{kit.heading.phase}} 1:**
|
|
49
|
+
|
|
50
|
+
- [ ] Pre-scan: [the files and modules to read before starting]
|
|
51
|
+
- [ ] Implementation complete and committable
|
|
52
|
+
- [ ] Tests written (or the reason none are needed)
|
|
53
|
+
- [ ] `/uncle-bob` — Clean Architecture review
|
|
54
|
+
- [ ] Documentation updated or created
|
|
55
|
+
- [ ] Plan and roadmap in sync
|
|
56
|
+
|
|
57
|
+
## Tests
|
|
58
|
+
|
|
59
|
+
Which tests are needed? Decide with the decision tree in `.claude/rules/kit/testing.md`.
|
|
60
|
+
|
|
61
|
+
| File | Kind | Why |
|
|
62
|
+
| ---- | ---- | --- |
|
|
63
|
+
|
|
64
|
+
> No tests needed → give the reason explicitly (e.g. "layout only, no logic branch").
|
|
65
|
+
|
|
66
|
+
## {{kit.heading.documentation}}
|
|
67
|
+
|
|
68
|
+
What gets documented after completion?
|
|
69
|
+
|
|
70
|
+
- [ ] ...
|
|
71
|
+
|
|
72
|
+
## Rejected Alternatives
|
|
73
|
+
|
|
74
|
+
| Idea | Reason |
|
|
75
|
+
| ---- | ------ |
|
|
76
|
+
|
|
77
|
+
## Risks
|
|
78
|
+
|
|
79
|
+
| Risk | Severity | Mitigation |
|
|
80
|
+
| ---- | -------- | ---------- |
|
|
81
|
+
|
|
82
|
+
## N — Architecture documentation (arc42 light)
|
|
83
|
+
|
|
84
|
+
> After the implementation: derive the architecture docs from the finished code.
|
|
85
|
+
|
|
86
|
+
| # | Task | Status |
|
|
87
|
+
| --- | ---- | ------ |
|
|
88
|
+
| N.1 | Context: external systems and interfaces | ⬜ |
|
|
89
|
+
| N.2 | Building blocks: services, packages, apps | ⬜ |
|
|
90
|
+
| N.3 | Runtime: significant flows as sequence diagrams | ⬜ |
|
|
91
|
+
| N.4 | Decisions: ADRs from the decisions table | ⬜ |
|
|
92
|
+
| N.5 | `ARCHITECTURE.md` of the touched units: new invariants and risks | ⬜ |
|