jackherd 1.14.0 → 1.15.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 (83) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +27 -19
  3. package/assets/icons/skills.svg +1 -0
  4. package/dist/config.d.ts +9 -1
  5. package/dist/config.js +24 -1
  6. package/dist/plan-format.d.ts +24 -1
  7. package/dist/plan-format.js +25 -5
  8. package/docs/plan-contract.md +84 -0
  9. package/docs/skills-dark.svg +15 -2
  10. package/docs/skills.svg +15 -2
  11. package/herdr-plugin/herdr-plugin.toml +1 -1
  12. package/herdr-plugin/restore-plans.ts +56 -6
  13. package/install.sh +1 -1
  14. package/kit/hooks/hooks/merge-guard/board-settings.ts +5 -2
  15. package/kit/hooks/hooks/merge-guard/pull-request.ts +28 -15
  16. package/kit/hooks/hooks/merge-guard/run.ts +6 -3
  17. package/kit/hooks/hooks/merge-guard/settings.ts +12 -1
  18. package/kit/hooks/hooks/model-log/transcript.ts +4 -7
  19. package/kit/hooks/hooks/open-question/record.ts +4 -3
  20. package/kit/hooks/hooks/shared/dialog-answers.ts +7 -3
  21. package/kit/hooks/hooks/shared/log-vocabulary.ts +13 -0
  22. package/kit/hooks/hooks/shared/transcript.ts +7 -4
  23. package/kit/hooks/hooks/task-hint/decision.ts +3 -2
  24. package/kit/hooks/hooks/task-hint/run.ts +2 -4
  25. package/kit/rules/rules/kit/pull-requests.md +1 -0
  26. package/kit/workflow/skills/orchestrate/SKILL.md +22 -9
  27. package/kit/workflow/skills/orchestrate/handover/index.ts +8 -2
  28. package/kit/workflow/skills/orchestrate/references/adopt-plan.md +51 -0
  29. package/kit/workflow/skills/orchestrate/references/plan-commit.md +5 -5
  30. package/package.json +1 -1
  31. package/schemas/jackherd.schema.json +19 -0
  32. package/src/check/command.ts +1 -1
  33. package/src/check/file-kinds.ts +10 -8
  34. package/src/check/parallel-report.ts +10 -3
  35. package/src/check/settings.ts +4 -0
  36. package/src/check/skill-budget-defaults.ts +17 -5
  37. package/src/check/skill-budgets.ts +49 -19
  38. package/src/claude/paths.ts +12 -0
  39. package/src/cli.ts +1 -0
  40. package/src/config/schema.ts +27 -0
  41. package/src/context/fill.ts +2 -1
  42. package/src/context/log-vocabulary.ts +22 -0
  43. package/src/context/session-log.ts +2 -1
  44. package/src/lane-prompt/build.ts +10 -5
  45. package/src/lane-prompt/command.ts +6 -1
  46. package/src/lane-prompt/input.ts +4 -4
  47. package/src/lane-prompt/texts.ts +18 -10
  48. package/src/plan-contract/command.ts +69 -0
  49. package/src/plan-contract/contract.ts +144 -0
  50. package/src/plan-format/dependencies.ts +1 -4
  51. package/src/plan-format/index.ts +4 -0
  52. package/src/plan-format/lanes.ts +11 -0
  53. package/src/plan-format/markdown.ts +6 -5
  54. package/src/plan-format/model-table.ts +27 -0
  55. package/src/plans/activity-read.ts +98 -0
  56. package/src/plans/ask-coordinator.ts +20 -11
  57. package/src/plans/layout.ts +49 -4
  58. package/src/plans/live.ts +17 -4
  59. package/src/plans/plan-panes.ts +25 -5
  60. package/src/plans/snapshot.ts +18 -8
  61. package/src/plans/subagents-read.ts +4 -3
  62. package/src/plans/subagents.ts +2 -1
  63. package/src/plans/texts.ts +27 -1
  64. package/src/settled/send.ts +80 -4
  65. package/src/shared/parse-json.ts +8 -0
  66. package/src/sync/command.ts +17 -10
  67. package/src/sync/foreign-skills.ts +40 -0
  68. package/src/sync/global-kit.ts +31 -21
  69. package/src/sync/init.ts +2 -2
  70. package/src/sync/kit-exclude.ts +39 -0
  71. package/src/sync/plan-file.ts +1 -1
  72. package/src/sync/render.ts +2 -1
  73. package/src/sync/report.ts +37 -2
  74. package/src/sync/sync-result.ts +5 -2
  75. package/src/sync/sync.ts +13 -5
  76. package/src/task-signals/claude-signals.ts +6 -5
  77. package/src/watch/command.ts +56 -17
  78. package/src/watch/lane-message.ts +1 -1
  79. package/src/watch/lane-progress.ts +51 -2
  80. package/src/watch/lane-report.ts +57 -2
  81. package/src/watch/lane-watch.ts +2 -8
  82. package/src/watch/task-parallel-touched.ts +9 -2
  83. package/src/watch/texts.ts +4 -0
@@ -1,9 +1,22 @@
1
1
  import { CONTEXT_LEVEL, type ContextLevel, contextLabel } from '../context'
2
2
  import { closeDryRunOf } from '../lane/close-command-line'
3
- import { currentPhase, isTickedUnmerged, type LaneProgress, phasesText } from '../plan-format'
3
+ import {
4
+ currentPhase,
5
+ isPhaseFinished,
6
+ isPhaseOpen,
7
+ isTickedUnmerged,
8
+ type LaneProgress,
9
+ parseDependencyTable,
10
+ parsePlanPhases,
11
+ phaseLabel,
12
+ phaseLabels,
13
+ phasesText,
14
+ } from '../plan-format'
4
15
  import { terminalSafe } from '../plan-format/terminal-text'
5
16
  import { CHECK_NAME_LENGTH, type ChecksState } from '../plans/pull-requests'
6
17
  import { shownWithMore } from '../plans/shown-with-more'
18
+ import { AGENT_STATUS } from '../terminal/types'
19
+ import type { PlanLanguage } from '../vocabulary'
7
20
  import type { AppointmentEvent } from './appointments'
8
21
  import type { ApprovalEvent } from './approvals'
9
22
  import type { CoordinatorRead } from './coordinator-fill'
@@ -12,7 +25,7 @@ import type { LaneFillEvent } from './lane-fill'
12
25
  import type { LaneModelEvent } from './lane-model'
13
26
  import type { LaneParallelEvent } from './lane-parallel'
14
27
  import { laneStalledLine } from './lane-stalled'
15
- import { AGENT_STATUS, type LaneSnapshot, NO_AGENT } from './lane-watch'
28
+ import { type LaneSnapshot, NO_AGENT } from './lane-watch'
16
29
  import { PLAN_MISSING_EVENT, type PlanMissingEvent, planMissingLine } from './plan-missing'
17
30
  import { PULL_REQUEST_STATE, type WatchedPullRequest } from './pull-requests'
18
31
  import { SOURCE_FIELD } from './source-fields'
@@ -370,6 +383,48 @@ export const blockedDutyLines = (stopped: readonly string[], current: LaneSnapsh
370
383
  .filter((lane) => current[lane]?.status === AGENT_STATUS.blocked)
371
384
  .map((lane) => `${terminalSafe(lane)} ${AGENT_STATUS.blocked}: ${texts.blockedDuty}`)
372
385
 
386
+ /** An open phase whose dependencies are all ticked off, with the phases it waited for. */
387
+ export type ReadyPhase = { id: string; after: string[] }
388
+
389
+ /**
390
+ * The phases of a plan that can start now, in table order: open, not worked on (`worked`, the ids of the phases the
391
+ * lanes work on), every phase of the `Depends on` cell finished and no external gate in it. Empty for a plan without
392
+ * a dependency table or a row without a phase in the plan.
393
+ */
394
+ export const readyPhasesOf = (markdown: string, language: PlanLanguage, worked: ReadonlySet<string>): ReadyPhase[] => {
395
+ const table = parseDependencyTable(markdown, language) ?? []
396
+ const phases = parsePlanPhases(markdown, language)
397
+ const finished = new Set(phases.filter(isPhaseFinished).map(({ id }) => id))
398
+ const open = new Set(phases.filter(isPhaseOpen).map(({ id }) => id))
399
+ return table
400
+ .filter(
401
+ ({ id, after, external }) =>
402
+ open.has(id) && !worked.has(id) && external.length === 0 && after.every((each) => finished.has(each)),
403
+ )
404
+ .map(({ id, after }) => ({ id, after }))
405
+ }
406
+
407
+ const readyPhaseText = ({ id, after }: ReadyPhase): string =>
408
+ `${phaseLabel(id)}${after.length > 0 ? ` (after ${phaseLabels(after, ', ')})` : ''}`
409
+
410
+ /**
411
+ * What a resting lane (`done` or `idle`) with ready phases asks of the coordinator (`orchestrate` § 2): one line per such
412
+ * lane, naming every ready phase, to start it now; a pull request waiting for approval holds only the commit.
413
+ */
414
+ export const readyPhaseDutyLines = (
415
+ stopped: readonly string[],
416
+ current: LaneSnapshot,
417
+ ready: Readonly<Record<string, readonly ReadyPhase[]>>,
418
+ texts: WatchTexts,
419
+ ): string[] =>
420
+ stopped.flatMap((lane) => {
421
+ const phases = ready[lane] ?? []
422
+ if (phases.length === 0 || !RESTING_STATUSES.has(current[lane]?.status ?? '')) return []
423
+ return [
424
+ `${terminalSafe(lane)} ready: ${phases.map(readyPhaseText).join(', ')}: ${texts.readyPhaseDuty(phases.length)}`,
425
+ ]
426
+ })
427
+
373
428
  const takeOverNote = ({ replaced, stillRunning }: TakeOver): string => {
374
429
  if (stillRunning !== null) return ` (watcher ${stillRunning} did not stop)`
375
430
  return replaced === null ? '' : ` (replaced watcher ${replaced})`
@@ -1,7 +1,6 @@
1
- export type HerdrAgent = { name?: string; pane_id: string; agent_status: string; state_change_seq?: number }
1
+ import { AGENT_STATUS } from '../terminal/types'
2
2
 
3
- /** The herdr agent states; herdr's JSON delivers them as plain strings. */
4
- export const AGENT_STATUS = { working: 'working', blocked: 'blocked', done: 'done', idle: 'idle' } as const
3
+ export type HerdrAgent = { name?: string; pane_id: string; agent_status: string; state_change_seq?: number }
5
4
 
6
5
  /** The state a report gives a lane whose agent left the herdr listing. */
7
6
  export const LANE_GONE = 'no longer listed'
@@ -9,8 +8,6 @@ export const LANE_GONE = 'no longer listed'
9
8
  /** The state a report gives a lane that has no agent in herdr. */
10
9
  export const NO_AGENT = 'no agent'
11
10
 
12
- export type AgentStatus = (typeof AGENT_STATUS)[keyof typeof AGENT_STATUS]
13
-
14
11
  export type LaneState = { status: string; seq: number | null }
15
12
 
16
13
  export type LaneSnapshot = Record<string, LaneState>
@@ -20,9 +17,6 @@ export type SeenSeqs = Record<string, number | null>
20
17
 
21
18
  const STOP_STATUSES = new Set<string>([AGENT_STATUS.blocked, AGENT_STATUS.done, AGENT_STATUS.idle])
22
19
 
23
- export const isAgentStatus = (value: string): value is AgentStatus =>
24
- (Object.values(AGENT_STATUS) as string[]).includes(value)
25
-
26
20
  /** The state of each named lane in a `herdr agent list` answer; unnamed and other agents are left out. */
27
21
  export const toSnapshot = (agents: readonly HerdrAgent[], lanes: readonly string[]): LaneSnapshot =>
28
22
  Object.fromEntries(
@@ -1,13 +1,20 @@
1
1
  import { isAbsolute, relative } from 'node:path'
2
+ import { CLAUDE_TOOL, CONTENT_BLOCK_TYPE } from '../context/log-vocabulary'
2
3
  import { parseLogEntry } from '../context/session-log'
3
4
  import { isRecord } from '../shared/is-record'
4
5
 
5
- const WRITING_TOOLS: ReadonlySet<string> = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit'])
6
+ const WRITING_TOOLS: ReadonlySet<string> = new Set([
7
+ CLAUDE_TOOL.edit,
8
+ CLAUDE_TOOL.write,
9
+ CLAUDE_TOOL.multiEdit,
10
+ CLAUDE_TOOL.notebookEdit,
11
+ ])
6
12
 
7
13
  const writtenPathsOf = (entry: Record<string, unknown>): string[] => {
8
14
  const content = isRecord(entry.message) && Array.isArray(entry.message.content) ? entry.message.content : []
9
15
  return content.flatMap((block) => {
10
- if (!isRecord(block) || block.type !== 'tool_use' || !WRITING_TOOLS.has(String(block.name))) return []
16
+ if (!isRecord(block) || block.type !== CONTENT_BLOCK_TYPE.toolUse || !WRITING_TOOLS.has(String(block.name)))
17
+ return []
11
18
  const input = isRecord(block.input) ? block.input : {}
12
19
  const path = input.file_path ?? input.notebook_path
13
20
  return typeof path === 'string' ? [path] : []
@@ -32,6 +32,8 @@ export type WatchTexts = {
32
32
  planMissingDuty: string
33
33
  /** What a lane's call waiting for approval asks of the coordinator, with the mark (request id and call hash) the question must carry. */
34
34
  approvalDuty: (id: string, hash: string) => string
35
+ /** What a resting lane with ready phases asks of the coordinator, with how many phases are ready. */
36
+ readyPhaseDuty: (count: number) => string
35
37
  /** The standing duty while the main checkout is behind its base branch on the remote (`origin/main`), with the count of commits. */
36
38
  mainCheckoutBehind: (remoteBase: string, count: number) => string
37
39
  /** The standing duty while the main checkout has another branch than the base checked out (null for a detached head). */
@@ -76,6 +78,8 @@ export const watchTexts = (dialogLanguage: PlanLanguage): WatchTexts => ({
76
78
  "the herd shows the base ref's plan, not this one: run § 2 step 0 for it, copying from the lane's worktree",
77
79
  laneModelDuty:
78
80
  'switch it while idle: /exit, then claude --resume <session> --model <id> in the same pane (§ 2 step 6)',
81
+ readyPhaseDuty: (count) =>
82
+ `start ${count === 1 ? 'it' : 'them'} now (§ 2); a waiting PR approval holds only the commit`,
79
83
  mainCheckoutBehind: (remoteBase, count) =>
80
84
  `main checkout behind ${remoteBase} by ${count} (git merge --ff-only ${remoteBase})`,
81
85
  mainCheckoutOffBase: (branch, baseBranch) => `main checkout on ${branch ?? 'a detached head'}, not ${baseBranch}`,