jackherd 1.9.0 → 1.10.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 +30 -0
- package/INSTALL.md +1 -1
- package/README.md +50 -32
- package/dist/{chunk-MCP4XJQH.js → chunk-5MQBCUQ5.js} +9 -4
- package/dist/config.d.ts +39 -4
- package/dist/config.js +67 -26
- package/dist/plan-format.d.ts +45 -6
- package/dist/plan-format.js +46 -6
- package/docs/workflow-dark.svg +67 -63
- package/docs/workflow.svg +67 -63
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/install.sh +1 -1
- package/kit/hooks/ask-rules.json +15 -0
- package/kit/hooks/hooks/ask-hint/decision.ts +82 -14
- package/kit/hooks/hooks/ask-hint/index.ts +15 -19
- package/kit/hooks/hooks/ask-hint/settings.ts +3 -18
- package/kit/hooks/hooks/git-guard/decision.ts +50 -102
- package/kit/hooks/hooks/merge-guard/always-ask.ts +63 -0
- package/kit/hooks/hooks/merge-guard/{plan-approval-settings.ts → board-settings.ts} +21 -67
- package/kit/hooks/hooks/merge-guard/check-free.ts +20 -0
- package/kit/hooks/hooks/merge-guard/decision.ts +127 -187
- package/kit/hooks/hooks/merge-guard/index.ts +6 -3
- package/kit/hooks/hooks/merge-guard/lane-plan.ts +79 -0
- package/kit/hooks/hooks/merge-guard/run.ts +59 -54
- package/kit/hooks/hooks/merge-guard/settings.ts +13 -1
- package/kit/hooks/hooks/merge-guard/standing-approval.ts +142 -0
- package/kit/hooks/hooks/open-question/index.ts +26 -0
- package/kit/hooks/hooks/open-question/record.ts +135 -0
- package/kit/hooks/hooks/open-question/run.ts +52 -0
- package/kit/hooks/hooks/open-question/store.ts +39 -0
- package/kit/hooks/hooks/sent-file/store.ts +3 -12
- package/kit/hooks/hooks/shared/config.ts +7 -0
- package/kit/hooks/hooks/shared/dialog-answers.ts +9 -9
- package/kit/hooks/hooks/shared/files.ts +7 -0
- package/kit/hooks/hooks/shared/lane.ts +1 -1
- package/kit/hooks/hooks/shared/newest-verdict.ts +10 -0
- package/kit/hooks/hooks/shared/private-directory.ts +56 -9
- package/kit/hooks/hooks/shared/program-position.ts +2 -75
- package/kit/hooks/hooks/shared/shell-aliases.ts +1 -71
- package/kit/hooks/hooks/{ask-hint → shared}/transcript.ts +37 -6
- package/kit/hooks/hooks/{merge-guard → shared}/xargs-program.ts +10 -8
- package/kit/hooks/hooks/task-hint/decision.ts +49 -0
- package/kit/hooks/hooks/task-hint/index.ts +79 -0
- package/kit/hooks/hooks/task-hint/run.ts +62 -0
- package/kit/hooks/hooks/task-hint/store.ts +51 -0
- package/kit/hooks/hooks.json +13 -5
- package/kit/review/agents/skill-review.md +34 -0
- package/kit/review/skills/pre-commit/SKILL.md +1 -1
- package/kit/workflow/skills/follow/SKILL.md +1 -8
- package/kit/workflow/skills/implement/SKILL.md +8 -6
- package/kit/workflow/skills/orchestrate/SKILL.md +210 -433
- package/kit/workflow/skills/orchestrate/handover/decision.ts +43 -8
- package/kit/workflow/skills/orchestrate/handover/index.ts +32 -4
- package/kit/workflow/skills/orchestrate/handover/standing-approval.ts +84 -0
- package/kit/workflow/skills/orchestrate/references/acceptance.md +15 -0
- package/kit/workflow/skills/orchestrate/references/close-lane.md +17 -0
- package/kit/workflow/skills/orchestrate/references/coordinator-handover.md +43 -17
- package/kit/workflow/skills/orchestrate/references/draft-review.md +19 -0
- package/kit/workflow/skills/orchestrate/references/events.md +86 -0
- package/kit/workflow/skills/orchestrate/references/herd.md +6 -6
- package/kit/workflow/skills/orchestrate/references/lane-handover.md +18 -0
- package/kit/workflow/skills/orchestrate/references/lane-session.md +25 -0
- package/kit/workflow/skills/orchestrate/references/lane-setup.md +37 -0
- package/kit/workflow/skills/orchestrate/references/plan-commit.md +22 -10
- package/kit/workflow/skills/orchestrate/references/sensitive-input.md +5 -7
- package/kit/workflow/skills/plan/SKILL.md +6 -5
- package/kit/workflow/skills/plan/references/template-English.md +4 -4
- package/kit/workflow/skills/plan/references/template-German.md +4 -4
- package/package.json +3 -1
- package/recipes/README.md +2 -1
- package/recipes/sources.md +1 -1
- package/schemas/jackherd.schema.json +59 -5
- package/src/check/agent-text-reviewers.ts +23 -0
- package/src/check/command.ts +2 -0
- package/src/check/plan-structure-check.ts +49 -2
- package/src/check/plan.ts +8 -1
- package/src/check/schema.ts +9 -0
- package/src/check/skill-budget-defaults.ts +55 -0
- package/src/check/skill-budgets.ts +185 -0
- package/src/cli.ts +1 -0
- package/src/config/auto-window.ts +8 -0
- package/src/config/link-kinds.ts +7 -1
- package/src/config/schema.ts +58 -24
- package/src/lane/close-board.ts +106 -0
- package/src/lane/close-check.ts +183 -0
- package/src/lane/close-command-line.ts +11 -0
- package/src/lane/close-command.ts +217 -0
- package/src/lane/close-options.ts +43 -0
- package/src/lane/close-survey.ts +83 -0
- package/src/lane/close.ts +141 -0
- package/src/lane/command.ts +13 -0
- package/src/lane-prompt/build.ts +74 -0
- package/src/lane-prompt/command.ts +87 -0
- package/src/lane-prompt/input.ts +116 -0
- package/src/lane-prompt/options.ts +68 -0
- package/src/lane-prompt/texts.ts +101 -0
- package/src/plan-format/clock.ts +8 -0
- package/src/plan-format/dependencies.ts +7 -4
- package/src/plan-format/index.ts +1 -0
- package/src/plan-format/short-names.ts +71 -0
- package/src/plan-format/waiting.ts +7 -2
- package/src/plans/appointment-rows.ts +59 -0
- package/src/plans/ask-coordinator.ts +77 -0
- package/src/plans/ask-target.ts +25 -0
- package/src/plans/child-stop.ts +51 -0
- package/src/plans/closable-records.ts +34 -0
- package/src/plans/command.ts +3 -2
- package/src/plans/fetch.ts +26 -3
- package/src/plans/git.ts +19 -5
- package/src/plans/herd-claim.ts +173 -0
- package/src/plans/herd-memory.ts +36 -25
- package/src/plans/lane-plans.ts +44 -13
- package/src/plans/lane-summary.ts +253 -45
- package/src/plans/layout-phase.ts +29 -7
- package/src/plans/layout-summary.ts +89 -18
- package/src/plans/layout.ts +262 -68
- package/src/plans/link-chips.ts +1 -0
- package/src/plans/live.ts +107 -20
- package/src/plans/phase-ids.ts +8 -0
- package/src/plans/phase-source.ts +15 -0
- package/src/plans/plan-cache.ts +28 -0
- package/src/plans/plan-heads.ts +54 -0
- package/src/plans/pull-requests.ts +17 -2
- package/src/plans/question-records.ts +48 -0
- package/src/plans/questions-read.ts +13 -0
- package/src/plans/read-cache.ts +8 -0
- package/src/plans/record-id.ts +4 -0
- package/src/plans/restart-decision.ts +13 -4
- package/src/plans/selection.ts +98 -18
- package/src/plans/sent-files-read.ts +6 -15
- package/src/plans/sent-files.ts +7 -14
- package/src/plans/settings.ts +2 -1
- package/src/plans/signalled-phase.ts +42 -0
- package/src/plans/signals-read.ts +12 -4
- package/src/plans/snapshot.ts +85 -62
- package/src/plans/sprint-export.ts +11 -11
- package/src/plans/sprint-page.ts +5 -5
- package/src/plans/subagent-phases.ts +95 -0
- package/src/plans/subagent-tasks.ts +51 -0
- package/src/plans/subagents-read.ts +66 -17
- package/src/plans/subagents.ts +64 -6
- package/src/plans/supervisor.ts +14 -4
- package/src/plans/symbols.ts +3 -0
- package/src/plans/task-name.ts +34 -0
- package/src/plans/texts.ts +23 -6
- package/src/plans/view.ts +17 -9
- package/src/plans/waiting-click.ts +192 -37
- package/src/plans/waiting-records.ts +39 -0
- package/src/plans/waiting-window.ts +8 -0
- package/src/preview/command.ts +25 -3
- package/src/shared/private-location.ts +89 -0
- package/src/shared/stat-cache.ts +48 -0
- package/src/sync/kit-source.ts +29 -10
- package/src/sync/kit.ts +2 -0
- package/src/sync/manifest.ts +2 -0
- package/src/sync/plan-ask-rules.ts +94 -0
- package/src/sync/plan-hooks.ts +2 -0
- package/src/sync/render.ts +3 -3
- package/src/sync/settings-hooks.ts +4 -1
- package/src/sync/sync-plan.ts +1 -0
- package/src/sync/sync.ts +24 -3
- package/src/task-signals/claude-read.ts +27 -10
- package/src/terminal/text.ts +8 -3
- package/src/version/skill-diff.ts +46 -0
- package/src/vocabulary/index.ts +16 -6
- package/src/waiting/command.ts +22 -7
- package/src/watch/appointments.ts +1 -2
- package/src/watch/asking-lanes.ts +40 -0
- package/src/watch/base-tip.ts +11 -0
- package/src/watch/command.ts +330 -105
- package/src/watch/heartbeat-progress.ts +6 -18
- package/src/watch/kit-update.ts +28 -9
- package/src/watch/lane-name.ts +6 -0
- package/src/watch/lane-progress.ts +40 -8
- package/src/watch/lane-report-file.ts +20 -5
- package/src/watch/lane-report.ts +32 -4
- package/src/watch/lane-sources.ts +31 -3
- package/src/watch/main-checkout.ts +34 -0
- package/src/watch/plan-badges-read.ts +13 -7
- package/src/watch/pull-requests.ts +6 -2
- package/src/watch/reference-paths.ts +87 -0
- package/src/watch/running-subagents.ts +43 -0
- package/src/watch/skill-change.ts +124 -32
- package/src/watch/source-change.ts +48 -0
- package/src/watch/sources.ts +6 -3
- package/src/watch/stream-output.ts +4 -3
- package/src/watch/task-parallel-auto.ts +208 -0
- package/src/watch/task-parallel-data-paths.ts +46 -0
- package/src/watch/task-parallel-input.ts +5 -15
- package/src/watch/task-parallel-read.ts +3 -1
- package/src/watch/task-parallel.ts +36 -7
- package/src/watch/task-signal-missing.ts +69 -0
- package/src/watch/task-signal-read.ts +29 -0
- package/src/watch/texts.ts +10 -0
- package/src/watch/update-available.ts +6 -3
- package/src/watch/watch-events.ts +23 -0
- package/src/watch/watcher-lock.ts +25 -29
- package/kit/hooks/hooks/merge-guard/plan-approval-record.ts +0 -23
- package/kit/hooks/hooks/merge-guard/plan-approval-sources.ts +0 -196
- package/kit/hooks/hooks/merge-guard/plan-approval.ts +0 -168
- package/kit/hooks/hooks/merge-guard/plan-growth.ts +0 -40
- package/kit/hooks/hooks/merge-guard/plan-pointers.ts +0 -194
- package/kit/hooks/hooks/shared/command-text-runners.ts +0 -71
- package/kit/hooks/hooks/shared/plan-open-phases.ts +0 -114
package/src/plans/selection.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { currentPhase, type LaneProgress } from '../plan-format'
|
|
2
2
|
import { AGENT_STATUS } from '../terminal/types'
|
|
3
3
|
import { chipUrlOf, isChipKey, planUrlOf } from './link-chips'
|
|
4
|
+
import { TEXTS } from './texts'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Whether a lane is drawn open. It is decided once, when the lane first appears (`autoOpen`, by its status then), and
|
|
@@ -8,6 +9,10 @@ import { chipUrlOf, isChipKey, planUrlOf } from './link-chips'
|
|
|
8
9
|
*/
|
|
9
10
|
export type LaneFold = { open: boolean }
|
|
10
11
|
|
|
12
|
+
/** The blocks on top of the herd, by the name their fold is remembered under. */
|
|
13
|
+
export const TOP_BLOCK = { waiting: 'waiting', appointments: 'appointments' } as const
|
|
14
|
+
export type TopBlockId = (typeof TOP_BLOCK)[keyof typeof TOP_BLOCK]
|
|
15
|
+
|
|
11
16
|
/**
|
|
12
17
|
* The row under the cursor and the expanded phases, both as keys, whether ticked phases and items are
|
|
13
18
|
* listed one by one or summed up in one line, whether the footer shows the symbol legend, and the
|
|
@@ -19,6 +24,8 @@ export type Selection = {
|
|
|
19
24
|
showDone: boolean
|
|
20
25
|
showLegend: boolean
|
|
21
26
|
laneFolds: ReadonlyMap<string, LaneFold>
|
|
27
|
+
/** Per folded top block, the keys of its points seen when it was folded; a block that is open has no entry. */
|
|
28
|
+
blockFolds?: Readonly<Partial<Record<TopBlockId, ReadonlySet<string>>>>
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
/** A lane as the folds need it: its name and the status its agent reports, null without an agent. */
|
|
@@ -70,12 +77,25 @@ export const PLANNED_KEY = 'planned'
|
|
|
70
77
|
|
|
71
78
|
export const phaseKey = (lane: string, stepIndex: number, phaseId: string): string => `${lane}:${stepIndex}:${phaseId}`
|
|
72
79
|
|
|
80
|
+
const PHASE_KEY_SHAPE = '[^@>#]+:\\d+:[^:#>]+'
|
|
81
|
+
|
|
82
|
+
const PHASE_KEY = new RegExp(`^${PHASE_KEY_SHAPE}$`)
|
|
83
|
+
|
|
84
|
+
/** Whether a key names a phase: not a lane, row or task key, which carry `@`, `>` or `#`. */
|
|
85
|
+
const isPhaseKey = (key: string): boolean => PHASE_KEY.test(key)
|
|
86
|
+
|
|
87
|
+
/** The key of a lane's `next` row, which opens to the phases that wait. */
|
|
88
|
+
export const nextKey = (lane: string): string => `next>${lane}`
|
|
89
|
+
|
|
90
|
+
/** The key of a lane's `✓` row, which opens to the finished phases. */
|
|
91
|
+
export const finishedKey = (lane: string): string => `done>${lane}`
|
|
92
|
+
|
|
73
93
|
const TICKED_KEY_PREFIX = 'ticked>'
|
|
74
94
|
|
|
75
95
|
/** The key of the "N ticked" line of an open phase; it is clicked only, and a click lands on the phase. */
|
|
76
96
|
export const tickedKey = (phase: string): string => `${TICKED_KEY_PREFIX}${phase}`
|
|
77
97
|
|
|
78
|
-
const ITEM_KEY =
|
|
98
|
+
const ITEM_KEY = new RegExp(`^(${PHASE_KEY_SHAPE})#(\\d+)$`)
|
|
79
99
|
|
|
80
100
|
/**
|
|
81
101
|
* The key of a task row: its phase's key and the task's index in `phase.items`; a click or Enter opens its text.
|
|
@@ -173,13 +193,10 @@ const foldLane = (selection: Selection, lane: string, next: (open: boolean) => b
|
|
|
173
193
|
return { ...selection, laneFolds: new Map(selection.laneFolds).set(lane, { ...fold, open: next(fold.open) }) }
|
|
174
194
|
}
|
|
175
195
|
|
|
176
|
-
/**
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** Selects a row and opens it, or closes it when it is already open: a phase shows its items, a lane its phases. */
|
|
196
|
+
/**
|
|
197
|
+
* Selects the row under a click. A "N ticked" line flips `showDone` and selects its phase, a lane line flips the
|
|
198
|
+
* fold of its lane, any other row opens when closed and closes when open: a phase shows its items.
|
|
199
|
+
*/
|
|
183
200
|
export const applyClick = (selection: Selection, key: string): Selection => {
|
|
184
201
|
const phase = tickedPhaseOfKey(key)
|
|
185
202
|
if (phase !== null) return { ...selection, showDone: !selection.showDone, selected: phase }
|
|
@@ -187,7 +204,7 @@ export const applyClick = (selection: Selection, key: string): Selection => {
|
|
|
187
204
|
if (lane !== null) return { ...foldLane(selection, lane, flipped), selected: key }
|
|
188
205
|
const expanded = new Set(selection.expanded)
|
|
189
206
|
const itemPhase = itemPhaseOfKey(key)
|
|
190
|
-
if (itemPhase !== null && !expanded.has(itemPhase))
|
|
207
|
+
if (itemPhase !== null && !expanded.has(itemPhase)) addWithPhase(expanded, key)
|
|
191
208
|
else toggleIn(expanded, key)
|
|
192
209
|
return { ...selection, selected: key, expanded }
|
|
193
210
|
}
|
|
@@ -197,17 +214,30 @@ const toggle = (selection: Selection, keys: readonly string[]): Selection => {
|
|
|
197
214
|
return current ? applyClick(selection, current) : selection
|
|
198
215
|
}
|
|
199
216
|
|
|
200
|
-
|
|
217
|
+
/** Adds a key to the expanded ones, with the phase of a task. */
|
|
218
|
+
const addWithPhase = (expanded: Set<string>, key: string): void => {
|
|
219
|
+
expanded.add(key)
|
|
220
|
+
const itemPhase = itemPhaseOfKey(key)
|
|
221
|
+
if (itemPhase !== null) expanded.add(itemPhase)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const removeKey = (expanded: Set<string>, key: string): void => {
|
|
225
|
+
expanded.delete(key)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Applies a change to the selected row: `nextFold` to the fold of a lane, `change` to the expanded keys for any other row. */
|
|
229
|
+
const changeSelected = (
|
|
230
|
+
selection: Selection,
|
|
231
|
+
keys: readonly string[],
|
|
232
|
+
nextFold: (open: boolean) => boolean,
|
|
233
|
+
change: (expanded: Set<string>, key: string) => void,
|
|
234
|
+
): Selection => {
|
|
201
235
|
const current = selectedKey(selection, keys)
|
|
202
236
|
if (!current) return selection
|
|
203
237
|
const lane = laneOfKey(current)
|
|
204
|
-
if (lane !== null) return { ...foldLane(selection, lane,
|
|
238
|
+
if (lane !== null) return { ...foldLane(selection, lane, nextFold), selected: current }
|
|
205
239
|
const expanded = new Set(selection.expanded)
|
|
206
|
-
|
|
207
|
-
expanded.add(current)
|
|
208
|
-
const itemPhase = itemPhaseOfKey(current)
|
|
209
|
-
if (itemPhase !== null) expanded.add(itemPhase)
|
|
210
|
-
} else expanded.delete(current)
|
|
240
|
+
change(expanded, current)
|
|
211
241
|
return { ...selection, selected: current, expanded }
|
|
212
242
|
}
|
|
213
243
|
|
|
@@ -218,9 +248,59 @@ export const toggledExpansion = (selection: Selection, key: string): Selection =
|
|
|
218
248
|
return { ...selection, expanded }
|
|
219
249
|
}
|
|
220
250
|
|
|
221
|
-
|
|
251
|
+
/**
|
|
252
|
+
* Whether `block` is drawn folded: it was folded and every point now open was there then. A point that arrived since
|
|
253
|
+
* opens it again, so folding never hides something new. Each block remembers its own points.
|
|
254
|
+
*/
|
|
255
|
+
export const isBlockFolded = ({ blockFolds }: Selection, block: TopBlockId, keys: readonly string[]): boolean => {
|
|
256
|
+
const folded = blockFolds?.[block]
|
|
257
|
+
return folded !== undefined && keys.every((key) => folded.has(key))
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The selection with the fold of each block forgotten that a new point opened, given the points each block holds now.
|
|
262
|
+
* Without it a block that opened for a new point would fold again by itself once that point is gone. The selection
|
|
263
|
+
* itself when no fold is forgotten.
|
|
264
|
+
*/
|
|
265
|
+
export const settledBlockFolds = (
|
|
266
|
+
selection: Selection,
|
|
267
|
+
keysOf: Readonly<Record<TopBlockId, readonly string[]>>,
|
|
268
|
+
): Selection => {
|
|
269
|
+
const folds = selection.blockFolds ?? {}
|
|
270
|
+
const opened = Object.values(TOP_BLOCK).filter(
|
|
271
|
+
(block) => folds[block] !== undefined && !isBlockFolded(selection, block, keysOf[block]),
|
|
272
|
+
)
|
|
273
|
+
if (opened.length === 0) return selection
|
|
274
|
+
const kept = Object.fromEntries(Object.entries(folds).filter(([block]) => !opened.includes(block as TopBlockId)))
|
|
275
|
+
const { blockFolds: _forgotten, ...rest } = selection
|
|
276
|
+
return Object.keys(kept).length > 0 ? { ...rest, blockFolds: kept } : rest
|
|
277
|
+
}
|
|
222
278
|
|
|
223
|
-
|
|
279
|
+
/** The selection with `block` folded over the points `keys` when it was open, and open when it was folded. */
|
|
280
|
+
export const toggledBlockFold = (selection: Selection, block: TopBlockId, keys: readonly string[]): Selection => {
|
|
281
|
+
if (!isBlockFolded(selection, block, keys)) {
|
|
282
|
+
return { ...selection, blockFolds: { ...selection.blockFolds, [block]: new Set(keys) } }
|
|
283
|
+
}
|
|
284
|
+
const { blockFolds, ...rest } = selection
|
|
285
|
+
const { [block]: _folded, ...otherFolds } = blockFolds ?? {}
|
|
286
|
+
return Object.keys(otherFolds).length > 0 ? { ...rest, blockFolds: otherFolds } : rest
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const expand = (selection: Selection, keys: readonly string[]): Selection =>
|
|
290
|
+
changeSelected(selection, keys, opened, addWithPhase)
|
|
291
|
+
|
|
292
|
+
const collapse = (selection: Selection, keys: readonly string[]): Selection =>
|
|
293
|
+
changeSelected(selection, keys, closed, removeKey)
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The line for a press of `a` that changes nothing on screen, null for any other input. The key lists the ticked
|
|
297
|
+
* tasks inside an expanded phase, so it needs one among the `keys` drawn.
|
|
298
|
+
*/
|
|
299
|
+
export const showDoneNotice = (selection: Selection, input: string, keys: readonly string[]): string | null => {
|
|
300
|
+
if (!isInput(INPUT.showDone, input)) return null
|
|
301
|
+
const phaseOpen = keys.some((key) => isPhaseKey(key) && selection.expanded.has(key))
|
|
302
|
+
return phaseOpen ? null : TEXTS.showDoneNeedsPhase
|
|
303
|
+
}
|
|
224
304
|
|
|
225
305
|
export const applyKey = (selection: Selection, input: string, keys: readonly string[]): Selection => {
|
|
226
306
|
if (isInput(INPUT.up, input)) return move(selection, keys, -1)
|
|
@@ -1,22 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { isSentFileRecord, type SentFileRecord } from './sent-files'
|
|
1
|
+
import { closableRecordLog } from './closable-records'
|
|
2
|
+
import { isSentFileRecord } from './sent-files'
|
|
4
3
|
|
|
5
|
-
const
|
|
4
|
+
const log = closableRecordLog('sent', isSentFileRecord, ({ sentAt }) => sentAt)
|
|
6
5
|
|
|
7
6
|
/** Every record of the sent-file log in the main checkout, oldest first; nothing when the directory is missing or unreadable. */
|
|
8
|
-
export const readSentRecords =
|
|
9
|
-
readRecords(directoryOf(root), isSentFileRecord, ({ sentAt }) => sentAt)
|
|
10
|
-
|
|
11
|
-
const closeWhere = (root: string, isChosen: (record: SentFileRecord) => boolean, now: number): number => {
|
|
12
|
-
const open = readSentRecords(root).filter((record) => record.closedAt === null && isChosen(record))
|
|
13
|
-
for (const record of open) writeRecordAtomically(directoryOf(root), { ...record, closedAt: now })
|
|
14
|
-
return open.length
|
|
15
|
-
}
|
|
7
|
+
export const readSentRecords = log.read
|
|
16
8
|
|
|
17
9
|
/** Marks the named records `closedAt` and returns how many it closed; one closed already, or unknown, counts for nothing. */
|
|
18
|
-
export const closeSentRecords =
|
|
19
|
-
closeWhere(root, (record) => ids.includes(record.id), now)
|
|
10
|
+
export const closeSentRecords = log.close
|
|
20
11
|
|
|
21
12
|
/** Marks every open record `closedAt` and returns how many it closed. */
|
|
22
|
-
export const closeAllSentRecords =
|
|
13
|
+
export const closeAllSentRecords = log.closeAll
|
package/src/plans/sent-files.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { basename } from 'node:path'
|
|
2
|
-
import {
|
|
2
|
+
import { clockText, DAY_MS } from '../plan-format/clock'
|
|
3
3
|
import { terminalSafe } from '../plan-format/terminal-text'
|
|
4
|
-
import type
|
|
4
|
+
import { WAITING_KIND, type WaitingItem } from '../plan-format/waiting'
|
|
5
|
+
import { isRecordId } from './record-id'
|
|
5
6
|
import { laneDirectoryOf } from './unlisted-lanes'
|
|
7
|
+
import { isStillWaiting } from './waiting-window'
|
|
6
8
|
|
|
7
9
|
/** The record of a send of files to the user, as the kit hook `sent-file` writes it to `.jackherd/sent/`. */
|
|
8
10
|
export type SentFileRecord = {
|
|
@@ -19,12 +21,8 @@ export type SentFileRecord = {
|
|
|
19
21
|
/** A badge for a sent file: a `file:` address and the file's name. */
|
|
20
22
|
export type SentChip = { url: string; label: string }
|
|
21
23
|
|
|
22
|
-
const ID = /^[0-9a-f]{12}$/
|
|
23
24
|
/** The address of a published artifact, as the hook reads it from the tool's answer, as a whole. */
|
|
24
25
|
const ARTIFACT_ADDRESS = /^https:\/\/claude\.ai\/(?:code\/)?artifact\/[A-Za-z0-9_-]+$/
|
|
25
|
-
const DAY_MS = 86_400_000
|
|
26
|
-
/** A send waits on the user this long at most; the coordinator closes it earlier, with the answer. */
|
|
27
|
-
const WAITING_DAYS = 3
|
|
28
26
|
const SHOWN_PER_LANE = 5
|
|
29
27
|
|
|
30
28
|
/** A published artifact is recorded by its address; a sent file by its path. */
|
|
@@ -49,7 +47,7 @@ export const isSentFileRecord = (value: unknown): value is SentFileRecord => {
|
|
|
49
47
|
const record = value as Record<string, unknown>
|
|
50
48
|
return (
|
|
51
49
|
typeof record.id === 'string' &&
|
|
52
|
-
|
|
50
|
+
isRecordId(record.id) &&
|
|
53
51
|
isStringArray(record.files) &&
|
|
54
52
|
record.files.every(isRecordedFile) &&
|
|
55
53
|
(record.caption === null || typeof record.caption === 'string') &&
|
|
@@ -96,18 +94,13 @@ export const sentChipsByLane = (
|
|
|
96
94
|
return lanes
|
|
97
95
|
}
|
|
98
96
|
|
|
99
|
-
const clockText = (at: number): string => {
|
|
100
|
-
const date = new Date(at)
|
|
101
|
-
return `${twoDigits(date.getDate())}.${twoDigits(date.getMonth() + 1)}. ${twoDigits(date.getHours())}:${twoDigits(date.getMinutes())}`
|
|
102
|
-
}
|
|
103
|
-
|
|
104
97
|
const itemOf = (record: SentFileRecord): WaitingItem => {
|
|
105
98
|
const [first] = record.files
|
|
106
99
|
const more = record.files.length > 1 ? ` +${record.files.length - 1}` : ''
|
|
107
100
|
return {
|
|
108
101
|
since: clockText(record.sentAt),
|
|
109
102
|
item: `${labelOf(first, record.caption)}${more}`,
|
|
110
|
-
kind:
|
|
103
|
+
kind: WAITING_KIND.file,
|
|
111
104
|
id: record.id,
|
|
112
105
|
open: true,
|
|
113
106
|
url: addressOf(first),
|
|
@@ -117,5 +110,5 @@ const itemOf = (record: SentFileRecord): WaitingItem => {
|
|
|
117
110
|
/** What the sends that no one closed wait on: one open item per record younger than three days, oldest first. */
|
|
118
111
|
export const sentWaitingItems = (records: readonly SentFileRecord[], now: number): WaitingItem[] =>
|
|
119
112
|
latestPerFile(records)
|
|
120
|
-
.filter(({ closedAt, sentAt }) => closedAt
|
|
113
|
+
.filter(({ closedAt, sentAt }) => isStillWaiting(closedAt, sentAt, now))
|
|
121
114
|
.map(itemOf)
|
package/src/plans/settings.ts
CHANGED
|
@@ -9,7 +9,7 @@ export const CONFIG_PATH = PROJECT_CONFIG_PATH
|
|
|
9
9
|
|
|
10
10
|
export type PlansSettings = Pick<
|
|
11
11
|
JackherdConfig,
|
|
12
|
-
'planDirectory' | 'board' | 'worktreeDirectory' | 'baseBranch' | 'planLanguage' | 'responseLanguage'
|
|
12
|
+
'planDirectory' | 'board' | 'worktreeDirectory' | 'baseBranch' | 'planLanguage' | 'responseLanguage' | 'skillPrefix'
|
|
13
13
|
> & {
|
|
14
14
|
adapter: JackherdConfig['terminal']['adapter']
|
|
15
15
|
fetchIntervalMinutes: number
|
|
@@ -30,6 +30,7 @@ export const plansSettingsOf = (config: JackherdConfig): PlansSettings => ({
|
|
|
30
30
|
baseBranch: config.baseBranch,
|
|
31
31
|
planLanguage: config.planLanguage,
|
|
32
32
|
responseLanguage: config.responseLanguage,
|
|
33
|
+
skillPrefix: config.skillPrefix,
|
|
33
34
|
adapter: config.terminal.adapter,
|
|
34
35
|
fetchIntervalMinutes: config.plans.fetchIntervalMinutes,
|
|
35
36
|
linkKinds: config.plans.linkKinds,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { isPhaseOpen, type PhaseProgress, phaseLabel } from '../plan-format'
|
|
2
|
+
import type { ActiveTask } from './active-tasks'
|
|
3
|
+
|
|
4
|
+
/** `p24e` in a branch name, standing alone between separators: `feat/jackherd-p24e-task-signal`. */
|
|
5
|
+
const BRANCH_PHASE = /(?:^|[^a-z0-9])p(\d+[a-z]?)(?![a-z0-9])/i
|
|
6
|
+
|
|
7
|
+
/** The phase id a lane branch names (`feat/jackherd-p24e-task-signal` → `24e`); null for none. */
|
|
8
|
+
export const branchPhaseId = (branch: string | null): string | null =>
|
|
9
|
+
branch?.match(BRANCH_PHASE)?.[1]?.toLowerCase() ?? null
|
|
10
|
+
|
|
11
|
+
const newestStart = (tasks: readonly ActiveTask[]): number => Math.max(...tasks.map(({ startedAt }) => startedAt ?? 0))
|
|
12
|
+
|
|
13
|
+
/** The open phase with the newest task an agent reported; null when no open phase has one. */
|
|
14
|
+
const reportedPhase = (
|
|
15
|
+
phases: readonly PhaseProgress[],
|
|
16
|
+
reported: ReadonlyMap<string, readonly ActiveTask[]>,
|
|
17
|
+
): PhaseProgress | null =>
|
|
18
|
+
phases
|
|
19
|
+
.filter((phase) => isPhaseOpen(phase) && (reported.get(phase.id)?.length ?? 0) > 0)
|
|
20
|
+
.reduce<PhaseProgress | null>(
|
|
21
|
+
(best, phase) =>
|
|
22
|
+
best && newestStart(reported.get(best.id) ?? []) >= newestStart(reported.get(phase.id) ?? []) ? best : phase,
|
|
23
|
+
null,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The phase the lane says it works on (D112): the open phase with the newest reported task, else the open phase its
|
|
28
|
+
* branch names (`branchPhaseId`); null otherwise, and the ticks decide as before. A phase ticked to the end is never
|
|
29
|
+
* signalled, so a stale report or an old branch name cannot make finished work current.
|
|
30
|
+
*/
|
|
31
|
+
export const signalledPhase = (
|
|
32
|
+
phases: readonly PhaseProgress[],
|
|
33
|
+
reported: ReadonlyMap<string, readonly ActiveTask[]>,
|
|
34
|
+
branch: string | null,
|
|
35
|
+
): PhaseProgress | null => {
|
|
36
|
+
const id = branchPhaseId(branch)
|
|
37
|
+
return reportedPhase(phases, reported) ?? phases.find((phase) => phase.id === id && isPhaseOpen(phase)) ?? null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The sidebar title of a lane: the signalled phase as `P24d · 1/5`, else `fromProgress` (`shortTitle`). */
|
|
41
|
+
export const signalledTitle = (signalled: PhaseProgress | null, fromProgress: string | null): string | null =>
|
|
42
|
+
signalled ? `${phaseLabel(signalled.id)} · ${signalled.done}/${signalled.total}` : fromProgress
|
|
@@ -2,7 +2,8 @@ import { readdirSync, statSync } from 'node:fs'
|
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import { projectDirectoryOf } from '../context/session-log'
|
|
4
4
|
import { MS_PER_MINUTE } from '../plan-format/clock'
|
|
5
|
-
import {
|
|
5
|
+
import { dropUnused, type StatCache } from '../shared/stat-cache'
|
|
6
|
+
import { readClaudeSignals, type SignalCache } from '../task-signals/claude-read'
|
|
6
7
|
import { recordSignals } from '../task-signals/record'
|
|
7
8
|
import { readTaskRecords } from '../task-signals/record-store'
|
|
8
9
|
import type { TaskSignal } from '../task-signals/types'
|
|
@@ -41,14 +42,21 @@ const recentSessionLogs = (project: string, notOlderThan: number): string[] => {
|
|
|
41
42
|
/**
|
|
42
43
|
* What agents report doing, as `TaskSignal`s: the tasks in progress in the recent Claude Code sessions of the main
|
|
43
44
|
* checkout (the coordinator and its subagents) and of every worktree, and the open `jh task` records. Which lane a
|
|
44
|
-
* signal belongs to is decided by its `cwd`, not by where it was read.
|
|
45
|
+
* signal belongs to is decided by its `cwd`, not by where it was read. A `cache` kept between calls spares parsing a
|
|
46
|
+
* log whose modification time and size did not change; logs this call did not look at are dropped from it.
|
|
45
47
|
*/
|
|
46
|
-
export const readAllSignals = (
|
|
48
|
+
export const readAllSignals = (
|
|
49
|
+
{ projects, root, worktrees }: SignalSource,
|
|
50
|
+
now: number,
|
|
51
|
+
cache: StatCache<TaskSignal[]> = new Map(),
|
|
52
|
+
): TaskSignal[] => {
|
|
47
53
|
const notOlderThan = now - STALE_ACTIVITY_MINUTES * MS_PER_MINUTE
|
|
54
|
+
const round: SignalCache = { entries: cache, used: new Set<string>() }
|
|
48
55
|
const claude = [root, ...subdirectories(worktrees)].flatMap((directory) =>
|
|
49
56
|
recentSessionLogs(join(projects, projectDirectoryOf(directory)), notOlderThan).flatMap((path) =>
|
|
50
|
-
readClaudeSignals(path, { notOlderThan }),
|
|
57
|
+
readClaudeSignals(path, { notOlderThan, cache: round }),
|
|
51
58
|
),
|
|
52
59
|
)
|
|
60
|
+
dropUnused(round.entries, round.used)
|
|
53
61
|
return [...claude, ...recordSignals(readTaskRecords(root))]
|
|
54
62
|
}
|