jackherd 1.6.0 → 1.7.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.
Files changed (152) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/INSTALL.md +176 -0
  3. package/README.md +260 -315
  4. package/assets/branding/README.md +7 -3
  5. package/assets/branding/jackherd-logo-dark-1024.png +0 -0
  6. package/assets/branding/jackherd-logo-dark-512.png +0 -0
  7. package/assets/branding/jackherd-logo-dark.svg +1 -1
  8. package/assets/icons/bark-dark.svg +1 -0
  9. package/assets/icons/bark.svg +1 -0
  10. package/assets/icons/bowl-dark.svg +1 -0
  11. package/assets/icons/bowl.svg +1 -0
  12. package/assets/icons/decide-dark.svg +1 -0
  13. package/assets/icons/decide.svg +1 -0
  14. package/assets/icons/ear-dark.svg +1 -0
  15. package/assets/icons/ear.svg +1 -0
  16. package/assets/icons/lanes-dark.svg +1 -0
  17. package/assets/icons/lanes.svg +1 -0
  18. package/assets/icons/plan-dark.svg +1 -0
  19. package/assets/icons/plan.svg +1 -0
  20. package/assets/icons/space.svg +1 -0
  21. package/dist/{chunk-QJSXHHQ3.js → chunk-OBUWTQ6Z.js} +25 -0
  22. package/dist/config.d.ts +14 -3
  23. package/dist/config.js +111 -10
  24. package/dist/plan-format.d.ts +180 -77
  25. package/dist/plan-format.js +292 -93
  26. package/docs/lanes-dark.svg +50 -0
  27. package/docs/lanes.svg +50 -1
  28. package/docs/skills-dark.svg +65 -0
  29. package/docs/skills.svg +65 -1
  30. package/docs/workflow-dark.svg +138 -0
  31. package/docs/workflow.svg +135 -169
  32. package/herdr-plugin/herdr-plugin.toml +6 -6
  33. package/herdr-plugin/open-plans.ts +1 -1
  34. package/herdr-plugin/restore-plans.ts +7 -7
  35. package/install.sh +1 -1
  36. package/kit/hooks/hooks/approval-gate/decision.ts +61 -0
  37. package/kit/hooks/hooks/approval-gate/herdr-panes.ts +24 -0
  38. package/kit/hooks/hooks/approval-gate/index.ts +69 -0
  39. package/kit/hooks/hooks/approval-gate/lane.ts +14 -0
  40. package/kit/hooks/hooks/approval-gate/request.ts +111 -0
  41. package/kit/hooks/hooks/approval-gate/run.ts +94 -0
  42. package/kit/hooks/hooks/approval-gate/settings.ts +26 -0
  43. package/kit/hooks/hooks/approval-gate/store.ts +70 -0
  44. package/kit/hooks/hooks/approval-gate/transcripts.ts +40 -0
  45. package/kit/hooks/hooks/merge-guard/approval.ts +11 -74
  46. package/kit/hooks/hooks/merge-guard/run.ts +2 -1
  47. package/kit/hooks/hooks/model-gate/agent-definition.ts +13 -0
  48. package/kit/hooks/hooks/model-gate/index.ts +58 -0
  49. package/kit/hooks/hooks/model-gate/run.ts +63 -0
  50. package/kit/hooks/hooks/model-log/index.ts +60 -0
  51. package/kit/hooks/hooks/model-log/run.ts +57 -0
  52. package/kit/hooks/hooks/model-log/store.ts +40 -0
  53. package/kit/hooks/hooks/model-log/transcript.ts +37 -0
  54. package/kit/hooks/hooks/shared/checkout.ts +22 -0
  55. package/kit/hooks/hooks/shared/command.ts +6 -0
  56. package/kit/hooks/hooks/shared/config.ts +21 -0
  57. package/kit/hooks/hooks/shared/dialog-answer.ts +5 -0
  58. package/kit/hooks/hooks/shared/dialog-answers.ts +74 -0
  59. package/kit/hooks/hooks/shared/files.ts +19 -0
  60. package/kit/hooks/hooks/shared/fresh.ts +4 -0
  61. package/kit/hooks/hooks/shared/json.ts +4 -0
  62. package/kit/hooks/hooks/shared/model-family.ts +48 -0
  63. package/kit/hooks/hooks/shared/plan-model-table.ts +41 -0
  64. package/kit/hooks/hooks/shared/private-directory.ts +29 -0
  65. package/kit/hooks/hooks/shared/repository-directory.ts +14 -0
  66. package/kit/hooks/hooks/shared/subagent-phase.ts +99 -0
  67. package/kit/hooks/hooks.json +25 -0
  68. package/kit/workflow/skills/follow/SKILL.md +4 -3
  69. package/kit/workflow/skills/implement/SKILL.md +6 -6
  70. package/kit/workflow/skills/orchestrate/SKILL.md +113 -30
  71. package/kit/workflow/skills/plan/SKILL.md +5 -5
  72. package/kit/workflow/skills/start/SKILL.md +2 -2
  73. package/package.json +4 -1
  74. package/schemas/jackherd.schema.json +43 -5
  75. package/src/check/command.ts +31 -4
  76. package/src/check/deferral-arrows.ts +1 -1
  77. package/src/check/parallel-report.ts +23 -0
  78. package/src/check/plan-structure-check.ts +120 -53
  79. package/src/cli.ts +8 -2
  80. package/src/config/nested-quantifier.ts +73 -0
  81. package/src/config/repository-directory.ts +11 -0
  82. package/src/config/schema.ts +63 -8
  83. package/src/plan-format/board-prose.ts +36 -0
  84. package/src/plan-format/dependencies.ts +227 -0
  85. package/src/plan-format/documentation-gate.ts +16 -0
  86. package/src/plan-format/index.ts +23 -1
  87. package/src/plan-format/lanes.ts +35 -1
  88. package/src/plan-format/model-column.ts +52 -0
  89. package/src/plan-format/phases.ts +16 -1
  90. package/src/plan-format/plan-structure.ts +10 -28
  91. package/src/plan-format/progress.ts +7 -29
  92. package/src/plans/command.ts +3 -3
  93. package/src/plans/current-phase.ts +71 -0
  94. package/src/plans/ensure-panes.ts +2 -2
  95. package/src/plans/herd-command.ts +4 -0
  96. package/src/plans/lane-discovery.ts +14 -2
  97. package/src/plans/lane-tickets.ts +69 -0
  98. package/src/plans/layout.ts +182 -88
  99. package/src/plans/live.ts +5 -1
  100. package/src/plans/phase-models-read.ts +37 -0
  101. package/src/plans/phase-models.ts +70 -0
  102. package/src/plans/plan-panes.ts +8 -7
  103. package/src/plans/pull-requests.ts +78 -4
  104. package/src/plans/selection.ts +1 -40
  105. package/src/plans/settings.ts +9 -3
  106. package/src/plans/shown-with-more.ts +5 -0
  107. package/src/plans/snapshot.ts +82 -8
  108. package/src/plans/sprint-html.ts +2 -2
  109. package/src/plans/supervisor.ts +5 -4
  110. package/src/plans/symbols.ts +9 -6
  111. package/src/plans/texts.ts +30 -13
  112. package/src/plans/view.ts +1 -0
  113. package/src/sync/herdr-plugin.ts +1 -1
  114. package/src/sync/next-steps.ts +1 -1
  115. package/src/sync/render.ts +2 -1
  116. package/src/sync/setup-questions.ts +2 -2
  117. package/src/terminal/text.ts +4 -0
  118. package/src/terminal/types.ts +1 -1
  119. package/src/vocabulary/index.ts +39 -0
  120. package/src/watch/approvals.ts +96 -0
  121. package/src/watch/command.ts +154 -50
  122. package/src/watch/failed-tests.ts +52 -0
  123. package/src/watch/failure-read.ts +56 -0
  124. package/src/watch/failures.ts +107 -0
  125. package/src/watch/feedback-item.ts +21 -0
  126. package/src/watch/feedback-read.ts +2 -1
  127. package/src/watch/feedback.ts +37 -28
  128. package/src/watch/gh-process.ts +53 -0
  129. package/src/watch/gh.ts +10 -20
  130. package/src/watch/heartbeat-progress.ts +1 -1
  131. package/src/watch/lane-model.ts +54 -0
  132. package/src/watch/lane-parallel.ts +46 -0
  133. package/src/watch/lane-progress.ts +28 -4
  134. package/src/watch/lane-report.ts +62 -9
  135. package/src/watch/lane-sources.ts +1 -1
  136. package/src/watch/no-finding.ts +47 -0
  137. package/src/watch/plan-badges-read.ts +19 -7
  138. package/src/watch/plan-badges.ts +59 -7
  139. package/src/watch/plan-views.ts +5 -5
  140. package/src/watch/previews.ts +3 -9
  141. package/src/watch/pull-requests.ts +12 -2
  142. package/src/watch/recorded-reads.ts +25 -0
  143. package/src/watch/source-fields.ts +4 -0
  144. package/src/watch/source-tickets.ts +99 -0
  145. package/src/watch/sources.ts +62 -15
  146. package/src/watch/texts.ts +21 -1
  147. package/src/watch/watch-defaults.ts +8 -0
  148. package/src/watch/watch-events.ts +64 -4
  149. package/src/watch/watcher-lock.ts +1 -1
  150. package/src/watch/workflow-runs.ts +5 -7
  151. package/docs/lanes.mmd +0 -9
  152. package/docs/skills.mmd +0 -10
@@ -1,8 +1,12 @@
1
1
  import { CHECKS, type ChecksState } from '../plans/pull-requests'
2
+ import { APPROVAL_EVENT, type ApprovalEvent } from './approvals'
2
3
  import { type CoordinatorLevel, type CoordinatorReading, isContextLevel, levelStep } from './coordinator-fill'
4
+ import { emptyFailureState, type FailureState, failedTestsOf, isFailureState } from './failures'
3
5
  import { emptyFeedbackState, FEEDBACK_EVENT, type FeedbackEvent, type FeedbackState, isFeedbackState } from './feedback'
4
6
  import { isRecord } from './json-guards'
5
7
  import { isLaneLevels, LANE_FILL_EVENT, type LaneFillEvent, type LaneLevels } from './lane-fill'
8
+ import { isLaneModels, LANE_MODEL_EVENT, type LaneModelEvent, type LaneModels } from './lane-model'
9
+ import { isLaneParallels, LANE_PARALLEL_EVENT, type LaneParallelEvent, type LaneParallels } from './lane-parallel'
6
10
  import { type LaneSnapshot, type SeenSeqs, seenSeqsOf } from './lane-watch'
7
11
  import { emptyPreviewState, isPreviewState, PREVIEW_EVENT, type PreviewEvent, type PreviewState } from './previews'
8
12
  import { PULL_REQUEST_STATE, type WatchedPullRequest } from './pull-requests'
@@ -21,10 +25,13 @@ export const WATCH_EVENT = {
21
25
  feedback: FEEDBACK_EVENT,
22
26
  preview: PREVIEW_EVENT,
23
27
  laneFill: LANE_FILL_EVENT,
28
+ laneModel: LANE_MODEL_EVENT,
29
+ laneParallel: LANE_PARALLEL_EVENT,
30
+ approval: APPROVAL_EVENT,
24
31
  source: SOURCE_EVENT,
25
32
  } as const
26
33
 
27
- export type PullRequestObservation = Omit<WatchedPullRequest, 'number' | 'branch'>
34
+ export type PullRequestObservation = Omit<WatchedPullRequest, 'number' | 'branch' | 'failedRuns'>
28
35
 
29
36
  /** The settled verdict of a pull request's checks on one head commit, the unit that fires once. */
30
37
  export type CheckVerdict = { headCommit: string; checks: ChecksState }
@@ -60,10 +67,18 @@ export type WatchState = {
60
67
  coordinatorLevel: CoordinatorLevel | null
61
68
  /** The feedback seen on the lanes' open pull requests; kept apart from `pullRequests`, it is read over REST. */
62
69
  feedback: FeedbackState
70
+ /** The failed tests read from the logs of the lanes' red pull requests, once per head commit; see `failures.ts`. */
71
+ failures: FailureState
63
72
  /** The previews seen on the lanes' open pull requests: per pull request the head last reported and the source versions. */
64
73
  previews: PreviewState
65
74
  /** The context level last read for each watched lane's agent; a rise above it fires `lane <name> <level>`. */
66
75
  laneLevels: LaneLevels
76
+ /** The differing model last reported for each watched lane's agent; a lane fitting its model column has none. */
77
+ laneModels: LaneModels
78
+ /** The phases last reported as able to run together for each watched lane; a lane without two has none. */
79
+ laneParallels: LaneParallels
80
+ /** The ids of the approval requests on disk when they were last read, each reported once while it is open. */
81
+ approvals: string[]
67
82
  /** Per configured source of `watch.sources`: the entries seen (`[id, fingerprint]`) and the start of a failure streak. */
68
83
  sources: SourceState
69
84
  /** Lane → change time of the report file last shown for it, null when it had none; a lane without an entry is unseen. */
@@ -75,12 +90,18 @@ export type WatchState = {
75
90
  /** The kinds of the events a pull request fires. */
76
91
  export const PULL_REQUEST_KINDS = [WATCH_EVENT.merged, WATCH_EVENT.checksFailed, WATCH_EVENT.checksPassed] as const
77
92
 
78
- /** A lane's pull request, or an own one without a lane, which names its branch instead. */
93
+ /**
94
+ * A lane's pull request, or an own one without a lane, which names its branch instead. `checks failed` carries the names
95
+ * of the failed checks and, for a lane's pull request, the tests read from the logs of the head commit, each only when
96
+ * there is one; no other kind carries either.
97
+ */
79
98
  export type PullRequestEvent = {
80
99
  kind: (typeof PULL_REQUEST_KINDS)[number]
81
100
  number: number
82
101
  headCommit: string
83
102
  mergeCommit: string | null
103
+ failedChecks?: string[]
104
+ failedTests?: string[]
84
105
  } & ({ lane: string } | { lane: null; branch: string })
85
106
 
86
107
  /** A run of a lane's merge, or without a lane: of an own pull request's merge (`pullRequest`), else started by hand. */
@@ -105,6 +126,9 @@ export type WatchEvent =
105
126
  | FeedbackEvent
106
127
  | PreviewEvent
107
128
  | LaneFillEvent
129
+ | LaneModelEvent
130
+ | LaneParallelEvent
131
+ | ApprovalEvent
108
132
  | SourceEvent
109
133
 
110
134
  export const isPullRequestEvent = (event: WatchEvent): event is PullRequestEvent =>
@@ -132,8 +156,12 @@ export const emptyWatchState = (): WatchState => ({
132
156
  laneBranches: {},
133
157
  coordinatorLevel: null,
134
158
  feedback: emptyFeedbackState(),
159
+ failures: emptyFailureState(),
135
160
  previews: emptyPreviewState(),
136
161
  laneLevels: {},
162
+ laneModels: {},
163
+ laneParallels: {},
164
+ approvals: [],
137
165
  sources: emptySourceState(),
138
166
  reportMtimes: {},
139
167
  statusLines: null,
@@ -169,8 +197,13 @@ const isWatchState = (value: Record<string, unknown>): value is WatchState =>
169
197
  everyValue(value.laneBranches, (entry) => typeof entry === 'string') &&
170
198
  (value.coordinatorLevel === null || isContextLevel(value.coordinatorLevel)) &&
171
199
  isFeedbackState(value.feedback) &&
200
+ isFailureState(value.failures) &&
172
201
  isPreviewState(value.previews) &&
173
202
  isLaneLevels(value.laneLevels) &&
203
+ isLaneModels(value.laneModels) &&
204
+ isLaneParallels(value.laneParallels) &&
205
+ Array.isArray(value.approvals) &&
206
+ value.approvals.every((id) => typeof id === 'string') &&
174
207
  isSourceState(value.sources) &&
175
208
  everyValue(value.reportMtimes, isSeq) &&
176
209
  (value.statusLines === null ||
@@ -189,8 +222,12 @@ export const parseWatchState = (json: string): WatchState => {
189
222
  laneBranches: {},
190
223
  coordinatorLevel: null,
191
224
  feedback: emptyFeedbackState(),
225
+ failures: emptyFailureState(),
192
226
  previews: emptyPreviewState(),
193
227
  laneLevels: {},
228
+ laneModels: {},
229
+ laneParallels: {},
230
+ approvals: [],
194
231
  sources: emptySourceState(),
195
232
  reportMtimes: {},
196
233
  statusLines: null,
@@ -260,8 +297,12 @@ export const recordWatchState = (previous: WatchState, observation: Observation,
260
297
  laneBranches: keptLaneBranches(previous, observation, github.pullRequests),
261
298
  coordinatorLevel: previous.coordinatorLevel,
262
299
  feedback: previous.feedback,
300
+ failures: previous.failures,
263
301
  previews: previous.previews,
264
302
  laneLevels: previous.laneLevels,
303
+ laneModels: previous.laneModels,
304
+ laneParallels: previous.laneParallels,
305
+ approvals: previous.approvals,
265
306
  sources: previous.sources,
266
307
  reportMtimes: previous.reportMtimes,
267
308
  statusLines: previous.statusLines,
@@ -317,16 +358,34 @@ const eventKindOf = (known: PullRequestRecord, observed: PullRequestSnapshot): P
317
358
  const laneOfBranch = (laneBranches: Readonly<Record<string, string>>, branch: string): string | undefined =>
318
359
  Object.keys(laneBranches).find((lane) => laneBranches[lane] === branch)
319
360
 
361
+ /** What a `checks failed` event adds: the failed checks' names, and a lane's failed tests; nothing for another kind. */
362
+ const failureDetails = (
363
+ kind: PullRequestEvent['kind'],
364
+ pullRequest: PullRequestSnapshot,
365
+ isLane: boolean,
366
+ failures: FailureState,
367
+ ): Pick<PullRequestEvent, 'failedChecks' | 'failedTests'> => {
368
+ if (kind !== WATCH_EVENT.checksFailed) return {}
369
+ const failedChecks = pullRequest.failedRuns.map(({ name }) => name)
370
+ const failedTests = isLane ? failedTestsOf(failures, pullRequest.number, pullRequest.headCommit) : []
371
+ return {
372
+ ...(failedChecks.length > 0 && { failedChecks }),
373
+ ...(failedTests.length > 0 && { failedTests }),
374
+ }
375
+ }
376
+
320
377
  /**
321
378
  * The merges and settled checks of the lanes' pull requests since `previous`, and of the user's own pull requests on
322
379
  * a branch without a lane, which name their branch. A pull request number never seen is a baseline, and so is an own
323
380
  * one the state does not mark as own yet. Checks fire once per head commit and settled verdict: a new head commit
324
- * fires when its checks settle, not while they are pending, and pending never clears what was reported.
381
+ * fires when its checks settle, not while they are pending, and pending never clears what was reported. A red verdict
382
+ * names the failed checks and, for a lane's pull request, the tests `failures` holds for its head commit.
325
383
  */
326
384
  export const pullRequestEvents = (
327
385
  previous: WatchState,
328
386
  current: readonly PullRequestSnapshot[],
329
387
  laneBranches: Readonly<Record<string, string>>,
388
+ failures: FailureState = emptyFailureState(),
330
389
  ): PullRequestEvent[] =>
331
390
  current.flatMap((pullRequest) => {
332
391
  const lane = laneOfBranch(laneBranches, pullRequest.branch)
@@ -336,7 +395,8 @@ export const pullRequestEvents = (
336
395
  if (!kind) return []
337
396
  const { number, headCommit, mergeCommit, branch } = pullRequest
338
397
  const owner = lane === undefined ? { lane: null, branch } : { lane }
339
- return [{ kind, ...owner, number, headCommit, mergeCommit }]
398
+ const details = failureDetails(kind, pullRequest, lane !== undefined, failures)
399
+ return [{ kind, ...owner, number, headCommit, mergeCommit, ...details }]
340
400
  })
341
401
 
342
402
  /** The lane each merge commit belongs to, for matching deploy runs to a lane's merge. */
@@ -95,7 +95,7 @@ const isAlive = (pid: number): boolean => {
95
95
  }
96
96
  }
97
97
 
98
- /** Where `ensureWatcherDirectory` puts the watcher's files, without creating anything: for readers such as the plans view. */
98
+ /** Where `ensureWatcherDirectory` puts the watcher's files, without creating anything: for readers such as the herd. */
99
99
  export const watcherDirectoryOf = (root: string): string =>
100
100
  join(tmpdir(), `${DIRECTORY_PREFIX}-${process.getuid?.() ?? 'user'}`, repositoryKey(root))
101
101
 
@@ -1,4 +1,6 @@
1
1
  import type { JackherdConfig } from '../config'
2
+ import { GITHUB_HOST } from '../plans/github'
3
+ import { runIdOfLink } from '../plans/pull-requests'
2
4
  import { isRecord, parseArray } from './json-guards'
3
5
 
4
6
  /** Which workflow's runs are deploys, and which environments count as their target (`watch.deploy`). */
@@ -37,8 +39,6 @@ export const WORKFLOW_RUN_FIELDS = 'databaseId,event,headSha,status,conclusion,c
37
39
 
38
40
  const RUN_LIMIT = '20'
39
41
 
40
- const RUN_LOG_PATH = /\/actions\/runs\/(\d+)\//
41
-
42
42
  const isGhWorkflowRun = (value: unknown): value is GhWorkflowRun =>
43
43
  isRecord(value) &&
44
44
  typeof value.databaseId === 'number' &&
@@ -89,8 +89,6 @@ export const runListArgs = (deploy: DeployConfig | undefined, baseBranch: string
89
89
  ]
90
90
  : null
91
91
 
92
- /** The run ID a deployment status's `log_url` (`…/actions/runs/<id>/job/<job>`) points at, or null. */
93
- export const runIdOfLogUrl = (logUrl: string): number | null => {
94
- const match = logUrl.match(RUN_LOG_PATH)
95
- return match ? Number(match[1]) : null
96
- }
92
+ /** The run ID a deployment status's `log_url` (`…/actions/runs/<id>/job/<job>`) points at, or null; see `runIdOfLink`. */
93
+ export const runIdOfLogUrl = (logUrl: string): number | null =>
94
+ runIdOfLink(logUrl, { host: GITHUB_HOST, repository: null })
package/docs/lanes.mmd DELETED
@@ -1,9 +0,0 @@
1
- flowchart LR
2
- coordinator[Coordinator<br/>/orchestrate] -- prompts --> laneA[Lane a<br/>worktree a]
3
- coordinator -- open decisions,<br/>one approval per PR --> user([You])
4
- coordinator -- prompts --> laneB[Lane b<br/>worktree b]
5
- laneA -- questions as text --> coordinator
6
- laneB -- questions as text --> coordinator
7
- watcher[jh watch] -- wakes --> coordinator
8
- laneA -. state, PR, checks .-> watcher
9
- laneB -. state, PR, checks .-> watcher
package/docs/skills.mmd DELETED
@@ -1,10 +0,0 @@
1
- flowchart LR
2
- grill["/grill<br/>settles decisions"] -- decision rows --> plan["/plan<br/>writes the plan"]
3
- plan -- phases, Writes,<br/>model per phase --> orchestrate["/orchestrate<br/>lanes in herdr"]
4
- plan -- one phase --> implement["/implement<br/>builds, ticks"]
5
- orchestrate -- each lane runs --> implement
6
- implement --> precommit["/pre-commit<br/>jh check"]
7
- precommit --> review["/review"]
8
- precommit --> spec["spec-review"]
9
- precommit -. routed by path .-> routed["uncle-bob · security-audit · devops"]
10
- diagnose["/diagnose<br/>bug, flaky failure"] -- red command --> implement