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.
- package/CHANGELOG.md +15 -0
- package/README.md +27 -19
- package/assets/icons/skills.svg +1 -0
- package/dist/config.d.ts +9 -1
- package/dist/config.js +24 -1
- package/dist/plan-format.d.ts +24 -1
- package/dist/plan-format.js +25 -5
- package/docs/plan-contract.md +84 -0
- package/docs/skills-dark.svg +15 -2
- package/docs/skills.svg +15 -2
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/herdr-plugin/restore-plans.ts +56 -6
- package/install.sh +1 -1
- package/kit/hooks/hooks/merge-guard/board-settings.ts +5 -2
- package/kit/hooks/hooks/merge-guard/pull-request.ts +28 -15
- package/kit/hooks/hooks/merge-guard/run.ts +6 -3
- package/kit/hooks/hooks/merge-guard/settings.ts +12 -1
- package/kit/hooks/hooks/model-log/transcript.ts +4 -7
- package/kit/hooks/hooks/open-question/record.ts +4 -3
- package/kit/hooks/hooks/shared/dialog-answers.ts +7 -3
- package/kit/hooks/hooks/shared/log-vocabulary.ts +13 -0
- package/kit/hooks/hooks/shared/transcript.ts +7 -4
- package/kit/hooks/hooks/task-hint/decision.ts +3 -2
- package/kit/hooks/hooks/task-hint/run.ts +2 -4
- package/kit/rules/rules/kit/pull-requests.md +1 -0
- package/kit/workflow/skills/orchestrate/SKILL.md +22 -9
- package/kit/workflow/skills/orchestrate/handover/index.ts +8 -2
- package/kit/workflow/skills/orchestrate/references/adopt-plan.md +51 -0
- package/kit/workflow/skills/orchestrate/references/plan-commit.md +5 -5
- package/package.json +1 -1
- package/schemas/jackherd.schema.json +19 -0
- package/src/check/command.ts +1 -1
- package/src/check/file-kinds.ts +10 -8
- package/src/check/parallel-report.ts +10 -3
- package/src/check/settings.ts +4 -0
- package/src/check/skill-budget-defaults.ts +17 -5
- package/src/check/skill-budgets.ts +49 -19
- package/src/claude/paths.ts +12 -0
- package/src/cli.ts +1 -0
- package/src/config/schema.ts +27 -0
- package/src/context/fill.ts +2 -1
- package/src/context/log-vocabulary.ts +22 -0
- package/src/context/session-log.ts +2 -1
- package/src/lane-prompt/build.ts +10 -5
- package/src/lane-prompt/command.ts +6 -1
- package/src/lane-prompt/input.ts +4 -4
- package/src/lane-prompt/texts.ts +18 -10
- package/src/plan-contract/command.ts +69 -0
- package/src/plan-contract/contract.ts +144 -0
- package/src/plan-format/dependencies.ts +1 -4
- package/src/plan-format/index.ts +4 -0
- package/src/plan-format/lanes.ts +11 -0
- package/src/plan-format/markdown.ts +6 -5
- package/src/plan-format/model-table.ts +27 -0
- package/src/plans/activity-read.ts +98 -0
- package/src/plans/ask-coordinator.ts +20 -11
- package/src/plans/layout.ts +49 -4
- package/src/plans/live.ts +17 -4
- package/src/plans/plan-panes.ts +25 -5
- package/src/plans/snapshot.ts +18 -8
- package/src/plans/subagents-read.ts +4 -3
- package/src/plans/subagents.ts +2 -1
- package/src/plans/texts.ts +27 -1
- package/src/settled/send.ts +80 -4
- package/src/shared/parse-json.ts +8 -0
- package/src/sync/command.ts +17 -10
- package/src/sync/foreign-skills.ts +40 -0
- package/src/sync/global-kit.ts +31 -21
- package/src/sync/init.ts +2 -2
- package/src/sync/kit-exclude.ts +39 -0
- package/src/sync/plan-file.ts +1 -1
- package/src/sync/render.ts +2 -1
- package/src/sync/report.ts +37 -2
- package/src/sync/sync-result.ts +5 -2
- package/src/sync/sync.ts +13 -5
- package/src/task-signals/claude-signals.ts +6 -5
- package/src/watch/command.ts +56 -17
- package/src/watch/lane-message.ts +1 -1
- package/src/watch/lane-progress.ts +51 -2
- package/src/watch/lane-report.ts +57 -2
- package/src/watch/lane-watch.ts +2 -8
- package/src/watch/task-parallel-touched.ts +9 -2
- package/src/watch/texts.ts +4 -0
package/src/watch/lane-report.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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})`
|
package/src/watch/lane-watch.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { AGENT_STATUS } from '../terminal/types'
|
|
2
2
|
|
|
3
|
-
|
|
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([
|
|
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 !==
|
|
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] : []
|
package/src/watch/texts.ts
CHANGED
|
@@ -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}`,
|