jackherd 1.7.1 → 1.8.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 +38 -0
- package/INSTALL.md +2 -0
- package/{NOTICE → NOTICE.txt} +1 -1
- package/README.md +94 -1033
- package/assets/icons/bark-dark.svg +1 -1
- package/assets/icons/bark.svg +1 -1
- package/assets/icons/bowl-dark.svg +1 -1
- package/assets/icons/bowl.svg +1 -1
- package/assets/icons/decide-dark.svg +1 -1
- package/assets/icons/decide.svg +1 -1
- package/assets/icons/ear-dark.svg +1 -1
- package/assets/icons/ear.svg +1 -1
- package/assets/icons/lanes-dark.svg +1 -1
- package/assets/icons/lanes.svg +1 -1
- package/assets/icons/plan-dark.svg +1 -1
- package/assets/icons/plan.svg +1 -1
- package/dist/{chunk-OBUWTQ6Z.js → chunk-M6DDZEW4.js} +24 -10
- package/dist/config.d.ts +11 -5
- package/dist/config.js +53 -21
- package/dist/plan-format.d.ts +73 -7
- package/dist/plan-format.js +180 -100
- package/docs/workflow-dark.svg +1 -1
- package/docs/workflow.svg +1 -1
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/install.sh +1 -1
- package/kit/hooks/hooks/approval-gate/herdr-panes.ts +2 -12
- package/kit/hooks/hooks/approval-gate/index.ts +10 -8
- package/kit/hooks/hooks/approval-gate/run.ts +21 -12
- package/kit/hooks/hooks/ask-hint/decision.ts +3 -3
- package/kit/hooks/hooks/git-guard/decision.ts +189 -15
- package/kit/hooks/hooks/git-guard/index.ts +13 -3
- package/kit/hooks/hooks/git-guard/push-target.ts +85 -0
- package/kit/hooks/hooks/git-guard/repository.ts +63 -0
- package/kit/hooks/hooks/merge-guard/check-free.ts +1 -13
- package/kit/hooks/hooks/merge-guard/decision.ts +342 -102
- package/kit/hooks/hooks/merge-guard/index.ts +6 -4
- package/kit/hooks/hooks/merge-guard/plain-path.ts +15 -0
- package/kit/hooks/hooks/merge-guard/plan-approval-record.ts +23 -0
- package/kit/hooks/hooks/merge-guard/plan-approval-settings.ts +142 -0
- package/kit/hooks/hooks/merge-guard/plan-approval-sources.ts +196 -0
- package/kit/hooks/hooks/merge-guard/plan-approval.ts +168 -0
- package/kit/hooks/hooks/merge-guard/plan-growth.ts +40 -0
- package/kit/hooks/hooks/merge-guard/plan-pointers.ts +194 -0
- package/kit/hooks/hooks/merge-guard/pull-request.ts +67 -21
- package/kit/hooks/hooks/merge-guard/run.ts +118 -29
- package/kit/hooks/hooks/merge-guard/settings.ts +8 -8
- package/kit/hooks/hooks/merge-guard/worker.ts +3 -3
- package/kit/hooks/hooks/shared/base-branch.ts +23 -0
- package/kit/hooks/hooks/shared/board-lanes.ts +93 -0
- package/kit/hooks/hooks/shared/command-text-runners.ts +71 -0
- package/kit/hooks/hooks/shared/command.ts +14 -2
- package/kit/hooks/hooks/shared/commit.ts +2 -0
- package/kit/hooks/hooks/shared/dialog-answers.ts +4 -1
- package/kit/hooks/hooks/shared/json.ts +10 -0
- package/kit/hooks/hooks/shared/lane.ts +57 -0
- package/kit/hooks/hooks/shared/once.ts +8 -0
- package/kit/hooks/hooks/shared/own-pane.ts +31 -0
- package/kit/hooks/hooks/shared/pane-agent.ts +24 -0
- package/kit/hooks/hooks/shared/plan-open-phases.ts +114 -0
- package/kit/hooks/hooks/shared/program-position.ts +148 -5
- package/kit/hooks/hooks/shared/shell-aliases.ts +258 -0
- package/kit/hooks/hooks/shared/shell-snapshots.ts +68 -0
- package/kit/hooks/hooks/shared/shell.ts +167 -20
- package/kit/hooks/hooks.json +11 -2
- package/kit/hooks/statusline.sh +29 -16
- package/kit/review/skills/diagnose/SKILL.md +1 -1
- package/kit/review/skills/review/SKILL.md +2 -0
- package/kit/rules/rules/kit/testing.md +2 -5
- package/kit/rules/rules/kit/writing-for-agents.md +3 -1
- package/kit/workflow/agents/spec-review.md +1 -1
- package/kit/workflow/skills/follow/SKILL.md +3 -3
- package/kit/workflow/skills/grill/SKILL.md +1 -1
- package/kit/workflow/skills/implement/SKILL.md +10 -11
- package/kit/workflow/skills/orchestrate/SKILL.md +155 -117
- package/kit/workflow/skills/orchestrate/references/coordinator-handover.md +17 -0
- package/kit/workflow/skills/orchestrate/references/herd.md +27 -0
- package/kit/workflow/skills/orchestrate/references/plan-commit.md +22 -0
- package/kit/workflow/skills/orchestrate/references/sensitive-input.md +23 -0
- package/kit/workflow/skills/plan/SKILL.md +5 -2
- package/kit/workflow/skills/plan/references/template-English.md +6 -0
- package/kit/workflow/skills/plan/references/template-German.md +6 -0
- package/package.json +3 -2
- package/recipes/README.md +21 -0
- package/recipes/github-issues.md +23 -0
- package/recipes/github-projects.md +22 -0
- package/recipes/jira-cli.md +26 -0
- package/recipes/jira-rest.md +24 -0
- package/recipes/linear.md +25 -0
- package/recipes/sentry.md +18 -0
- package/recipes/sources/jira.ts +51 -0
- package/recipes/sources/linear.ts +19 -0
- package/recipes/sources/sentry.ts +12 -0
- package/recipes/sources.md +80 -0
- package/recipes/status-page.md +19 -0
- package/schemas/jackherd.schema.json +42 -18
- package/src/check/command.ts +2 -2
- package/src/check/parallel-report.ts +19 -9
- package/src/cli.ts +1 -0
- package/src/config/context-shares.ts +130 -0
- package/src/config/read-config.ts +12 -4
- package/src/config/schema.ts +75 -20
- package/src/context/auto-compact.ts +68 -0
- package/src/context/command.ts +68 -17
- package/src/context/coordinator-name.ts +27 -9
- package/src/context/index.ts +0 -1
- package/src/context/level.ts +18 -11
- package/src/context/report.ts +6 -3
- package/src/plan-format/dependencies.ts +144 -32
- package/src/plan-format/index.ts +6 -0
- package/src/plan-format/lanes.ts +6 -0
- package/src/plan-format/progress.ts +19 -8
- package/src/plans/command.ts +14 -5
- package/src/plans/current-phase.ts +63 -7
- package/src/plans/current-task.ts +51 -0
- package/src/plans/ensure-panes.ts +25 -8
- package/src/plans/frame.ts +99 -0
- package/src/plans/git.ts +8 -0
- package/src/plans/herd-memory.ts +107 -0
- package/src/plans/lane-plans.ts +5 -3
- package/src/plans/lane-summary.ts +222 -0
- package/src/plans/layout-phase.ts +148 -0
- package/src/plans/layout-summary.ts +140 -0
- package/src/plans/layout.ts +164 -420
- package/src/plans/live.ts +11 -6
- package/src/plans/plan-panes.ts +15 -0
- package/src/plans/planned.ts +9 -42
- package/src/plans/selection.ts +2 -6
- package/src/plans/settings.ts +2 -7
- package/src/plans/snapshot.ts +38 -39
- package/src/plans/symbols.ts +2 -2
- package/src/plans/texts.ts +32 -31
- package/src/plans/view.ts +0 -1
- package/src/plans/worked-phases.ts +2 -2
- package/src/settled/board-row.ts +133 -0
- package/src/settled/check.ts +129 -0
- package/src/settled/command.ts +65 -0
- package/src/settled/evidence-version.ts +14 -0
- package/src/settled/options.ts +69 -0
- package/src/settled/send.ts +27 -0
- package/src/settled/settle.ts +87 -0
- package/src/settled/topics.ts +56 -0
- package/src/sync/render.ts +13 -4
- package/src/sync/sync.ts +25 -5
- package/src/terminal/herdr.ts +46 -10
- package/src/terminal/none.ts +1 -0
- package/src/terminal/types.ts +15 -2
- package/src/vocabulary/index.ts +47 -11
- package/src/watch/appointments.ts +205 -0
- package/src/watch/command.ts +62 -19
- package/src/watch/lane-report.ts +11 -3
- package/src/watch/texts.ts +35 -59
- package/src/watch/watch-events.ts +21 -28
- package/kit/hooks/hooks/approval-gate/lane.ts +0 -14
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util'
|
|
2
|
+
import { DEFAULT_CONTEXT_WINDOW } from './schema'
|
|
3
|
+
|
|
4
|
+
/** A config with its context shares in tokens, and a line for each block that changed. */
|
|
5
|
+
export type SharesInTokens = { raw: unknown; notes: string[] }
|
|
6
|
+
|
|
7
|
+
type JsonObject = Record<string, unknown>
|
|
8
|
+
|
|
9
|
+
type Pair = { share: string; tokens: string }
|
|
10
|
+
|
|
11
|
+
/** A share turned into tokens, and a share dropped because its token value is already set. */
|
|
12
|
+
type Conversion = Pair & { value: number; result: number }
|
|
13
|
+
type Drop = Pair & { value: number; kept: unknown }
|
|
14
|
+
|
|
15
|
+
type ConvertedBlock = { block: JsonObject; converted: Conversion[]; dropped: Drop[] }
|
|
16
|
+
|
|
17
|
+
const PAIRS: readonly Pair[] = [
|
|
18
|
+
{ share: 'warnAt', tokens: 'warnTokens' },
|
|
19
|
+
{ share: 'alertAt', tokens: 'alertTokens' },
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
const ROUNDING = 1_000
|
|
23
|
+
|
|
24
|
+
const SHARE_ENTRY = new RegExp(`"(${PAIRS.map(({ share }) => share).join('|')})"(\\s*:\\s*)(0?\\.\\d+|0)`, 'g')
|
|
25
|
+
|
|
26
|
+
const isRecord = (value: unknown): value is JsonObject =>
|
|
27
|
+
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
28
|
+
|
|
29
|
+
const isShare = (value: unknown): value is number => typeof value === 'number' && value > 0 && value < 1
|
|
30
|
+
|
|
31
|
+
const sizes = (value: unknown): number[] =>
|
|
32
|
+
isRecord(value) ? Object.values(value).filter((size): size is number => typeof size === 'number' && size > 0) : []
|
|
33
|
+
|
|
34
|
+
/** The window a share was most likely read against: the largest the config names, the default one included. */
|
|
35
|
+
const windowOf = (context: JsonObject): number => {
|
|
36
|
+
const fallback = typeof context.defaultWindow === 'number' ? context.defaultWindow : DEFAULT_CONTEXT_WINDOW
|
|
37
|
+
return Math.max(fallback, ...sizes(context.windows))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const shareInTokens = (share: number, window: number): number => Math.round((share * window) / ROUNDING) * ROUNDING
|
|
41
|
+
|
|
42
|
+
const hasShares = (block: JsonObject): boolean => PAIRS.some(({ share }) => isShare(block[share]))
|
|
43
|
+
|
|
44
|
+
/** One block with its shares in tokens, and what changed; null when it has no share. */
|
|
45
|
+
const convertBlock = (block: JsonObject, window: number): ConvertedBlock | null => {
|
|
46
|
+
if (!hasShares(block)) return null
|
|
47
|
+
const next: JsonObject = {}
|
|
48
|
+
const converted: Conversion[] = []
|
|
49
|
+
const dropped: Drop[] = []
|
|
50
|
+
for (const [key, value] of Object.entries(block)) {
|
|
51
|
+
const pair = PAIRS.find(({ share }) => share === key)
|
|
52
|
+
if (!pair || !isShare(value)) {
|
|
53
|
+
next[key] = value
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
if (block[pair.tokens] !== undefined) {
|
|
57
|
+
dropped.push({ ...pair, value, kept: block[pair.tokens] })
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
const result = shareInTokens(value, window)
|
|
61
|
+
next[pair.tokens] = result
|
|
62
|
+
converted.push({ ...pair, value, result })
|
|
63
|
+
}
|
|
64
|
+
return { block: next, converted, dropped }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The words for what changed in a block: `plans.context: warnAt 0.5 is now warnTokens 100000 (share of a … window).` */
|
|
68
|
+
const blockNote = (label: string, window: number, { converted, dropped }: ConvertedBlock): string => {
|
|
69
|
+
const conversions = converted.map(
|
|
70
|
+
({ share, value, tokens, result }, index) => `${share} ${value} ${index === 0 ? 'is now ' : ''}${tokens} ${result}`,
|
|
71
|
+
)
|
|
72
|
+
const drops = dropped.map(({ share, value, tokens, kept }) => `${share} ${value} dropped, ${tokens} ${kept} is set`)
|
|
73
|
+
const parts = [
|
|
74
|
+
...(conversions.length > 0 ? [`${conversions.join(', ')} (share of a ${window} window)`] : []),
|
|
75
|
+
...drops,
|
|
76
|
+
]
|
|
77
|
+
return `${label}: ${parts.join('; ')}.`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Before the thresholds were tokens, `plans.context` and its `coordinator` gave them as a share of the window
|
|
82
|
+
* (`warnAt`, `alertAt`); this turns such a config into tokens, so it never breaks in silence.
|
|
83
|
+
* `raw` with the shares of `plans.context` and `plans.context.coordinator` in tokens of the largest window the
|
|
84
|
+
* config names, rounded to whole thousands, and a line per block that changed. A token value already set wins
|
|
85
|
+
* over the share beside it. Anything else, a share out of range included, stays as it is for the schema to judge.
|
|
86
|
+
*/
|
|
87
|
+
export const contextSharesInTokens = (raw: unknown): SharesInTokens => {
|
|
88
|
+
if (!isRecord(raw) || !isRecord(raw.plans) || !isRecord(raw.plans.context)) return { raw, notes: [] }
|
|
89
|
+
const context = raw.plans.context
|
|
90
|
+
const window = windowOf(context)
|
|
91
|
+
const coordinator = isRecord(context.coordinator) ? convertBlock(context.coordinator, window) : null
|
|
92
|
+
const lane = convertBlock(context, window)
|
|
93
|
+
if (!lane && !coordinator) return { raw, notes: [] }
|
|
94
|
+
const nextContext = { ...(lane?.block ?? context), ...(coordinator ? { coordinator: coordinator.block } : {}) }
|
|
95
|
+
const next = { ...raw, plans: { ...raw.plans, context: nextContext } }
|
|
96
|
+
const notes = [
|
|
97
|
+
lane && blockNote('plans.context', window, lane),
|
|
98
|
+
coordinator && blockNote('plans.context.coordinator', window, coordinator),
|
|
99
|
+
]
|
|
100
|
+
return { raw: next, notes: notes.filter((note): note is string => !!note) }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The text with every `"warnAt": <share>` and `"alertAt": <share>` renamed and in tokens, in place. */
|
|
104
|
+
const replacedInPlace = (text: string, window: number): string =>
|
|
105
|
+
text.replace(SHARE_ENTRY, (_match, key: string, separator: string, value: string) => {
|
|
106
|
+
const tokensKey = PAIRS.find(({ share }) => share === key)?.tokens ?? key
|
|
107
|
+
return `"${tokensKey}"${separator}${shareInTokens(Number(value), window)}`
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const parsesAs = (text: string, expected: unknown): boolean => {
|
|
111
|
+
try {
|
|
112
|
+
return isDeepStrictEqual(JSON.parse(text), expected)
|
|
113
|
+
} catch {
|
|
114
|
+
return false
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A config file's text with its shares in tokens (see `contextSharesInTokens`). The keys are replaced in place, so
|
|
120
|
+
* the rest of the file stays byte for byte; when the result would not read as the converted values (a share beside
|
|
121
|
+
* a token value, the key inside a string), the file is written anew from them.
|
|
122
|
+
*/
|
|
123
|
+
export const contextSharesTextInTokens = (text: string): { text: string; notes: string[] } => {
|
|
124
|
+
const raw: unknown = JSON.parse(text)
|
|
125
|
+
const { raw: converted, notes } = contextSharesInTokens(raw)
|
|
126
|
+
if (notes.length === 0) return { text, notes }
|
|
127
|
+
const context = (raw as { plans: { context: JsonObject } }).plans.context
|
|
128
|
+
const inPlace = replacedInPlace(text, windowOf(context))
|
|
129
|
+
return { text: parsesAs(inPlace, converted) ? inPlace : `${JSON.stringify(converted, null, 2)}\n`, notes }
|
|
130
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { homedir } from 'node:os'
|
|
3
|
+
import { CLI_NAME } from '../version/release'
|
|
4
|
+
import { contextSharesInTokens } from './context-shares'
|
|
3
5
|
import { type RepositoryFacts, withDetected } from './detect'
|
|
4
6
|
import { legacyConfigNote } from './legacy-names'
|
|
5
7
|
import { globalConfigPath, locateConfigFile, PROJECT_CONFIG_PATH } from './location'
|
|
@@ -9,13 +11,19 @@ import { type JackherdConfig, parseConfigFile } from './schema'
|
|
|
9
11
|
/** A config with the values detection filled in, and a line for each value the repository did not decide. */
|
|
10
12
|
export type ResolvedConfig = { config: JackherdConfig; notes: string[] }
|
|
11
13
|
|
|
14
|
+
/** The line for context thresholds read as tokens from a file that still gives them as a share. */
|
|
15
|
+
const sharesNote = (path: string, notes: readonly string[]): string[] =>
|
|
16
|
+
notes.length > 0 ? [`${path} gives context thresholds as a share; \`${CLI_NAME} sync\` writes them as tokens.`] : []
|
|
17
|
+
|
|
12
18
|
/**
|
|
13
|
-
* Parses a config file's raw JSON, filling what it leaves out from the repository (see `withDetected`)
|
|
14
|
-
*
|
|
19
|
+
* Parses a config file's raw JSON, filling what it leaves out from the repository (see `withDetected`) and reading
|
|
20
|
+
* context thresholds still given as a share in tokens (see `contextSharesInTokens`); an invalid file throws with its
|
|
21
|
+
* path and the issues in words.
|
|
15
22
|
*/
|
|
16
23
|
const resolveConfig = (raw: unknown, facts: RepositoryFacts, path: string): ResolvedConfig => {
|
|
17
|
-
const
|
|
18
|
-
|
|
24
|
+
const shares = contextSharesInTokens(raw ?? {})
|
|
25
|
+
const detected = withDetected(shares.raw, facts)
|
|
26
|
+
return { config: parseConfigFile(detected.raw, path), notes: [...sharesNote(path, shares.notes), ...detected.notes] }
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
/**
|
package/src/config/schema.ts
CHANGED
|
@@ -45,7 +45,10 @@ const modelId = z
|
|
|
45
45
|
|
|
46
46
|
const command = z.string().min(1)
|
|
47
47
|
|
|
48
|
-
const
|
|
48
|
+
const tokenCount = z.number().int().positive()
|
|
49
|
+
|
|
50
|
+
/** The window `plans.context.defaultWindow` assumes when the config leaves it out. */
|
|
51
|
+
export const DEFAULT_CONTEXT_WINDOW = 200_000
|
|
49
52
|
|
|
50
53
|
/** A file below the main checkout, written into shell commands: relative, never leaving it, never an option, no quote or control character. */
|
|
51
54
|
const laneCopyPath = z.string().regex(
|
|
@@ -54,9 +57,10 @@ const laneCopyPath = z.string().regex(
|
|
|
54
57
|
'a relative path inside the checkout: no leading / or -, no .. segment, no quote or control character',
|
|
55
58
|
)
|
|
56
59
|
|
|
57
|
-
const warnBelowAlert = ({
|
|
60
|
+
const warnBelowAlert = ({ warnTokens, alertTokens }: { warnTokens: number; alertTokens: number }): boolean =>
|
|
61
|
+
warnTokens < alertTokens
|
|
58
62
|
|
|
59
|
-
const WARN_BELOW_ALERT = { message: '
|
|
63
|
+
const WARN_BELOW_ALERT = { message: 'warnTokens must be below alertTokens', path: ['warnTokens'] }
|
|
60
64
|
|
|
61
65
|
/** The lock file, written unquoted into git commands: a plain relative path, never an option. */
|
|
62
66
|
const lockFilePath = z
|
|
@@ -70,8 +74,8 @@ const lockFilePath = z
|
|
|
70
74
|
const branchName = z
|
|
71
75
|
.string()
|
|
72
76
|
.regex(
|
|
73
|
-
/^(?![-/.])(?!.*(?:\.\.|\/\/|@\{|\.lock(?:\/|$)))[A-Za-z0-9._/-]+(?<![./])$/,
|
|
74
|
-
'a branch name such as main: no leading - / or ., no .. and no spaces',
|
|
77
|
+
/^(?!HEAD$)(?![-/.])(?!.*(?:\.\.|\/\/|@\{|\.lock(?:\/|$)))[A-Za-z0-9._/-]+(?<![./])$/,
|
|
78
|
+
'a branch name such as main: not HEAD, no leading - / or ., no .. and no spaces',
|
|
75
79
|
)
|
|
76
80
|
|
|
77
81
|
/** A workflow file name as `gh run list --workflow` takes it: no path, no option, a YAML file. */
|
|
@@ -223,10 +227,10 @@ const hasUniqueNames = (sources: readonly { name: string }[]): boolean =>
|
|
|
223
227
|
|
|
224
228
|
const coordinatorSchema = z
|
|
225
229
|
.strictObject({
|
|
226
|
-
/**
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
|
|
230
|
+
/** Tokens in the coordinator's context from which the watcher reports `coordinator warn`. */
|
|
231
|
+
warnTokens: tokenCount.default(200_000),
|
|
232
|
+
/** Tokens in the coordinator's context from which the watcher reports `coordinator alert`. */
|
|
233
|
+
alertTokens: tokenCount.default(300_000),
|
|
230
234
|
})
|
|
231
235
|
.refine(warnBelowAlert, WARN_BELOW_ALERT)
|
|
232
236
|
|
|
@@ -253,10 +257,38 @@ const checkFreePattern = z
|
|
|
253
257
|
'a pattern with a whole path segment that is no wildcard, such as docs/**/*.md',
|
|
254
258
|
)
|
|
255
259
|
|
|
260
|
+
/**
|
|
261
|
+
* A path pattern that always asks before a merge under a plan approval: plain path words with wildcards, relative, no
|
|
262
|
+
* empty, `.` or `..` segment (GitHub lists no file with one, so such a pattern would never ask). Broad on purpose (`.github/**`, `**`), since a pattern here only ever adds a question; braces,
|
|
263
|
+
* classes and negation stay out, so the hook, which has no schema library, reads exactly what this accepts.
|
|
264
|
+
*/
|
|
265
|
+
const alwaysAskPattern = z
|
|
266
|
+
.string()
|
|
267
|
+
.regex(/^[A-Za-z0-9_.*/-]+$/, 'path words with * and ** only, such as .github/** or apps/*/src/**')
|
|
268
|
+
.refine(
|
|
269
|
+
(pattern) => pattern.split('/').every((segment) => segment !== '' && segment !== '.' && segment !== '..'),
|
|
270
|
+
'a relative path that stays inside the repository, without an empty or . segment',
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The name of a check as GitHub lists it for a commit (a job name such as `test` or `build (ubuntu, 22)`, or a status
|
|
275
|
+
* context such as `ci/legacy`): a visible character, no control character, at most 200 characters.
|
|
276
|
+
*/
|
|
277
|
+
const checkName = z
|
|
278
|
+
.string()
|
|
279
|
+
.max(200)
|
|
280
|
+
.refine(
|
|
281
|
+
(name) =>
|
|
282
|
+
name.trim() !== '' &&
|
|
283
|
+
![...name].some((character) => character.charCodeAt(0) < 0x20 || character.charCodeAt(0) === 0x7f),
|
|
284
|
+
'a check name as GitHub lists it, such as test',
|
|
285
|
+
)
|
|
286
|
+
|
|
256
287
|
const mergeSchema = z.strictObject({
|
|
257
288
|
/**
|
|
258
289
|
* Paths of pull requests that are merged without waiting for checks: when every file of the pull request matches one
|
|
259
|
-
* of them, `/orchestrate` merges at once and the merge guard does not ask
|
|
290
|
+
* of them, `/orchestrate` merges at once and the merge guard does not ask, for a merge bound to the head commit with
|
|
291
|
+
* `--match-head-commit`. Empty means every merge waits for checks.
|
|
260
292
|
*/
|
|
261
293
|
withoutChecks: z.array(checkFreePattern).default([]),
|
|
262
294
|
})
|
|
@@ -264,9 +296,10 @@ const mergeSchema = z.strictObject({
|
|
|
264
296
|
const guardSchema = z.strictObject({
|
|
265
297
|
/**
|
|
266
298
|
* The commands the merge guard hook asks the user about before they run, also behind options before the verb, a chain,
|
|
267
|
-
* a wrapper or `sh -c`. An empty list switches the
|
|
299
|
+
* a wrapper or `sh -c`. `gh api` asks only for a write to a merge, a file, a dispatch, a rerun or a ref. An empty list switches the
|
|
300
|
+
* guard off.
|
|
268
301
|
*/
|
|
269
|
-
commands: z.array(guardedCommand).default(['gh pr merge', 'gh workflow run']),
|
|
302
|
+
commands: z.array(guardedCommand).default(['gh pr merge', 'gh workflow run', 'gh run rerun', 'gh api']),
|
|
270
303
|
selfMerge: z
|
|
271
304
|
.strictObject({
|
|
272
305
|
/** Whether `/orchestrate` offers "merge it yourself after green checks" in the pull request question; off also stops the hook honouring such an answer. */
|
|
@@ -281,6 +314,25 @@ const guardSchema = z.strictObject({
|
|
|
281
314
|
waitMinutes: z.number().int().min(1).max(9).default(8),
|
|
282
315
|
})
|
|
283
316
|
.prefault({}),
|
|
317
|
+
planApproval: z
|
|
318
|
+
.strictObject({
|
|
319
|
+
/**
|
|
320
|
+
* Paths that always ask, on top of the ones jackherd brings (`.github/**`, `**\/migrations/**`, `**\/.env*`,
|
|
321
|
+
* `.claude/**`, `.jackherd/**`, `bunfig.toml`, `**\/CLAUDE.md`, `**\/AGENTS.md`, `.agents/**`, `.mcp.json`,
|
|
322
|
+
* `.husky/**`, `lefthook.yml`, the lockfiles of bun, npm, pnpm and yarn, `**\/.npmrc`, `**\/CODEOWNERS`, every
|
|
323
|
+
* package.json, and the CHANGELOG.md of every package without `"private": true`):
|
|
324
|
+
* a pull request that changes one is never merged on a plan approval alone. They add to that list, never replace
|
|
325
|
+
* it; an invalid entry turns plan approval off, so every merge asks.
|
|
326
|
+
*/
|
|
327
|
+
exceptPaths: z.array(alwaysAskPattern).default([]),
|
|
328
|
+
/**
|
|
329
|
+
* Checks that must be among the head commit's and green before a plan approval lets a merge pass, by their names
|
|
330
|
+
* on GitHub. Unset, the merge needs at least one check and every one green; set, also each of these. Either way
|
|
331
|
+
* GitHub must be able to merge it and the head must contain the base branch.
|
|
332
|
+
*/
|
|
333
|
+
requiredChecks: z.array(checkName).optional(),
|
|
334
|
+
})
|
|
335
|
+
.prefault({}),
|
|
284
336
|
})
|
|
285
337
|
|
|
286
338
|
const hintsSchema = z.strictObject({
|
|
@@ -290,15 +342,18 @@ const hintsSchema = z.strictObject({
|
|
|
290
342
|
|
|
291
343
|
const contextSchema = z
|
|
292
344
|
.strictObject({
|
|
293
|
-
/**
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
|
|
345
|
+
/** Tokens in a lane's context from which its fill shows yellow and `/implement` offers a new chat. */
|
|
346
|
+
warnTokens: tokenCount.default(250_000),
|
|
347
|
+
/** Tokens in a lane's context from which its fill shows red and the lane hands over at the next phase boundary. */
|
|
348
|
+
alertTokens: tokenCount.default(350_000),
|
|
349
|
+
/**
|
|
350
|
+
* Context window in tokens by full model ID, only the upper bound of the share a view shows (the thresholds are
|
|
351
|
+
* tokens); the log does not say which window a session ran with.
|
|
352
|
+
*/
|
|
353
|
+
windows: z.record(modelId, tokenCount).default({}),
|
|
299
354
|
/** The window assumed for a model not listed in `windows`. */
|
|
300
|
-
defaultWindow:
|
|
301
|
-
/** The coordinator's own thresholds, used by `jh context --coordinator`; the lanes keep `
|
|
355
|
+
defaultWindow: tokenCount.default(DEFAULT_CONTEXT_WINDOW),
|
|
356
|
+
/** The coordinator's own thresholds, used by `jh context --coordinator`; the lanes keep `warnTokens` and `alertTokens`. */
|
|
302
357
|
coordinator: coordinatorSchema.prefault({}),
|
|
303
358
|
})
|
|
304
359
|
.refine(warnBelowAlert, WARN_BELOW_ALERT)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Claude Code's auto-compact window: how full the context may get before it compacts. Set by
|
|
6
|
+
* `CLAUDE_CODE_AUTO_COMPACT_WINDOW` (a plain token count, ahead of everything else), by `--autocompact` for one
|
|
7
|
+
* launch (not readable here), or in a settings file as `modelSettings.<model>.autoCompactWindow` or
|
|
8
|
+
* `autoCompactWindow`, a token count or `auto`.
|
|
9
|
+
*/
|
|
10
|
+
export const AUTO_COMPACT_ENV = 'CLAUDE_CODE_AUTO_COMPACT_WINDOW'
|
|
11
|
+
|
|
12
|
+
/** The settings files of a project and of the user, highest precedence first; managed settings stay out. */
|
|
13
|
+
const PROJECT_SETTINGS = ['.claude/settings.local.json', '.claude/settings.json']
|
|
14
|
+
|
|
15
|
+
const USER_SETTINGS = 'settings.json'
|
|
16
|
+
|
|
17
|
+
const AUTO = 'auto'
|
|
18
|
+
|
|
19
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
20
|
+
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
21
|
+
|
|
22
|
+
const isTokenCount = (value: unknown): value is number =>
|
|
23
|
+
typeof value === 'number' && Number.isInteger(value) && value > 0
|
|
24
|
+
|
|
25
|
+
/** A file's window for `model`: a count, `auto` (the window tuned for the model), or undefined when it sets none. */
|
|
26
|
+
const windowIn = (settings: unknown, model: string): number | typeof AUTO | undefined => {
|
|
27
|
+
if (!isRecord(settings)) return undefined
|
|
28
|
+
const perModel = isRecord(settings.modelSettings) ? settings.modelSettings[model] : undefined
|
|
29
|
+
const candidates = [isRecord(perModel) ? perModel.autoCompactWindow : undefined, settings.autoCompactWindow]
|
|
30
|
+
return candidates.find((value): value is number | typeof AUTO => isTokenCount(value) || value === AUTO)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The auto-compact window a session of `model` runs with, from the variable or else the first of `settings`
|
|
35
|
+
* (highest precedence first) that sets one; null when none does or the first that does says `auto`.
|
|
36
|
+
*/
|
|
37
|
+
export const autoCompactWindow = (
|
|
38
|
+
model: string,
|
|
39
|
+
env: string | undefined,
|
|
40
|
+
settings: readonly unknown[],
|
|
41
|
+
): number | null => {
|
|
42
|
+
if (env && /^\d+$/.test(env) && Number(env) > 0) return Number(env)
|
|
43
|
+
for (const file of settings) {
|
|
44
|
+
const window = windowIn(file, model)
|
|
45
|
+
if (window !== undefined) return window === AUTO ? null : window
|
|
46
|
+
}
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** One line when Claude Code would compact before the context reaches `alertTokens`; null otherwise. */
|
|
51
|
+
export const compactsBeforeAlertNote = (window: number | null, alertTokens: number): string | null =>
|
|
52
|
+
window !== null && window <= alertTokens
|
|
53
|
+
? `auto-compact window ${window} is not above alertTokens ${alertTokens}: Claude Code compacts before the hand-over`
|
|
54
|
+
: null
|
|
55
|
+
|
|
56
|
+
const readJson = (path: string): unknown[] => {
|
|
57
|
+
try {
|
|
58
|
+
return [JSON.parse(readFileSync(path, 'utf8'))]
|
|
59
|
+
} catch {
|
|
60
|
+
return []
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The settings of `projectRoot` and of the user's Claude directory, highest precedence first; unreadable ones left out. */
|
|
65
|
+
export const readClaudeSettings = (projectRoot: string, userDirectory: string): unknown[] => [
|
|
66
|
+
...PROJECT_SETTINGS.flatMap((path) => readJson(join(projectRoot, path))),
|
|
67
|
+
...readJson(join(userDirectory, USER_SETTINGS)),
|
|
68
|
+
]
|
package/src/context/command.ts
CHANGED
|
@@ -1,17 +1,68 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { basename, join } from 'node:path'
|
|
3
|
+
import { CLAUDE_DIRECTORY } from '../claude/paths'
|
|
2
4
|
import { mainCheckout, worktreeRoot } from '../plans/git'
|
|
3
|
-
import { readSettings } from '../plans/settings'
|
|
4
|
-
import { laneStatusSourceFor } from '../terminal'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { type PlansSettings, readSettings } from '../plans/settings'
|
|
6
|
+
import { type LaneAgent, laneStatusSourceFor } from '../terminal'
|
|
7
|
+
import { AUTO_COMPACT_ENV, autoCompactWindow, compactsBeforeAlertNote, readClaudeSettings } from './auto-compact'
|
|
8
|
+
import { resolveCoordinatorName } from './coordinator-name'
|
|
9
|
+
import { type ContextOptions, coordinatorSettings, parseContextOptions } from './options'
|
|
10
|
+
import { type ContextReport, contextReport, noValueNote, unnamedPaneNote } from './report'
|
|
8
11
|
import { agentLog } from './session-log'
|
|
9
12
|
|
|
13
|
+
type ResolvedAgents = { lane: string; named: LaneAgent[]; agents: LaneAgent[] }
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The line when the session's auto-compact window is not above the alert threshold, read from the settings of
|
|
17
|
+
* `projectRoot` and the user. `env` is the session's auto-compact variable, known only for the session the command
|
|
18
|
+
* runs in, since another agent's environment is not readable.
|
|
19
|
+
*/
|
|
20
|
+
const autoCompactNote = (
|
|
21
|
+
report: ContextReport | null,
|
|
22
|
+
alertTokens: number,
|
|
23
|
+
projectRoot: string,
|
|
24
|
+
env: string | undefined,
|
|
25
|
+
): string | null => {
|
|
26
|
+
if (!report) return null
|
|
27
|
+
const settings = readClaudeSettings(projectRoot, join(homedir(), CLAUDE_DIRECTORY))
|
|
28
|
+
return compactsBeforeAlertNote(autoCompactWindow(report.model, env, settings), alertTokens)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The lane the command reports on and the agents to look for it in. With `--coordinator` and no agent of that name,
|
|
33
|
+
* the pane the command runs in is adopted under it.
|
|
34
|
+
*/
|
|
35
|
+
const resolveAgents = (
|
|
36
|
+
options: ContextOptions,
|
|
37
|
+
settings: PlansSettings,
|
|
38
|
+
root: string,
|
|
39
|
+
worktree: string,
|
|
40
|
+
): ResolvedAgents => {
|
|
41
|
+
const source = laneStatusSourceFor(settings.adapter)
|
|
42
|
+
const named = source.listAgents()
|
|
43
|
+
const coordinatorName = resolveCoordinatorName(
|
|
44
|
+
basename(root),
|
|
45
|
+
named.map((agent) => agent.name),
|
|
46
|
+
)
|
|
47
|
+
const lane = options.lane ?? (options.coordinator ? coordinatorName : basename(worktree))
|
|
48
|
+
const pane = process.env.HERDR_PANE_ID
|
|
49
|
+
const adopt =
|
|
50
|
+
options.coordinator && pane && !named.some((agent) => agent.name === lane) ? { pane, name: lane } : undefined
|
|
51
|
+
return { lane, named, agents: adopt ? source.listAgents(adopt) : named }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const printNotes = ({ lane, named, agents }: ResolvedAgents, report: ContextReport | null): void => {
|
|
55
|
+
if (agents.length > named.length) console.error(unnamedPaneNote(lane))
|
|
56
|
+
if (report === null) console.error(noValueNote(agents, lane))
|
|
57
|
+
}
|
|
58
|
+
|
|
10
59
|
/**
|
|
11
60
|
* `jh context [<name>] [--coordinator]`: prints how full the context of an agent is as one line of JSON
|
|
12
61
|
* (`tokens`, `window`, `share`, `model`, `level`), or `null` when there is no value. The name defaults to the
|
|
13
|
-
* worktree the command runs in, with `--coordinator` to `
|
|
62
|
+
* worktree the command runs in, with `--coordinator` to `jack-<repository>`, or to `orchestrator-<repository>` while an
|
|
63
|
+
* agent still carries that old name. `--coordinator` also takes
|
|
14
64
|
* the level from the coordinator's thresholds (`plans.context.coordinator`). Reads the session log, never writes it.
|
|
65
|
+
* One more line on stderr when Claude Code's auto-compact window is not above the alert threshold.
|
|
15
66
|
*/
|
|
16
67
|
export const run = async (args: string[]): Promise<number> => {
|
|
17
68
|
try {
|
|
@@ -19,18 +70,18 @@ export const run = async (args: string[]): Promise<number> => {
|
|
|
19
70
|
const root = mainCheckout(process.cwd())
|
|
20
71
|
const worktree = worktreeRoot(process.cwd())
|
|
21
72
|
const settings = readSettings([worktree, root])
|
|
22
|
-
const
|
|
73
|
+
const resolved = resolveAgents(options, settings, root, worktree)
|
|
23
74
|
const context = options.coordinator ? coordinatorSettings(settings.context) : settings.context
|
|
24
|
-
const
|
|
25
|
-
const named = source.listAgents()
|
|
26
|
-
const pane = process.env.HERDR_PANE_ID
|
|
27
|
-
const adopt =
|
|
28
|
-
options.coordinator && pane && !named.some((agent) => agent.name === lane) ? { pane, name: lane } : undefined
|
|
29
|
-
const agents = adopt ? source.listAgents(adopt) : named
|
|
30
|
-
const report = contextReport(agents, lane, agentLog, context)
|
|
75
|
+
const report = contextReport(resolved.agents, resolved.lane, agentLog, context)
|
|
31
76
|
console.log(JSON.stringify(report))
|
|
32
|
-
|
|
33
|
-
|
|
77
|
+
printNotes(resolved, report)
|
|
78
|
+
const compactNote = autoCompactNote(
|
|
79
|
+
report,
|
|
80
|
+
context.alertTokens,
|
|
81
|
+
options.coordinator ? root : worktree,
|
|
82
|
+
options.lane ? undefined : process.env[AUTO_COMPACT_ENV],
|
|
83
|
+
)
|
|
84
|
+
if (compactNote) console.error(compactNote)
|
|
34
85
|
return 0
|
|
35
86
|
} catch (error) {
|
|
36
87
|
console.error(error instanceof Error ? error.message : String(error))
|
|
@@ -1,16 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
/** The prefix of the coordinator's herdr name, and the one it had before, kept as an alias for running sessions. */
|
|
2
|
+
export const COORDINATOR_NAME_PREFIX = { current: 'jack', legacy: 'orchestrator' } as const
|
|
3
|
+
|
|
4
|
+
type CoordinatorNamePrefix = (typeof COORDINATOR_NAME_PREFIX)[keyof typeof COORDINATOR_NAME_PREFIX]
|
|
5
|
+
|
|
2
6
|
const MAX_LENGTH = 32
|
|
3
7
|
|
|
4
|
-
|
|
5
|
-
* The herdr name a coordinator takes when none is given: `orchestrator-<repository>`. herdr names are unique
|
|
6
|
-
* across all workspaces, so two repositories cannot both run a coordinator called `orchestrator`. The name
|
|
7
|
-
* fits `[a-z][a-z0-9_-]{0,31}`; a repository name that leaves nothing usable gives plain `orchestrator`.
|
|
8
|
-
*/
|
|
9
|
-
export const defaultCoordinatorName = (repository: string): string => {
|
|
8
|
+
const nameWith = (prefix: CoordinatorNamePrefix, repository: string): string => {
|
|
10
9
|
const slug = repository
|
|
11
10
|
.toLowerCase()
|
|
12
11
|
.replace(/[^a-z0-9_-]+/g, '-')
|
|
13
12
|
.replace(/^-+|-+$/g, '')
|
|
14
|
-
if (!slug) return
|
|
15
|
-
return `${
|
|
13
|
+
if (!slug) return prefix
|
|
14
|
+
return `${prefix}-${slug}`.slice(0, MAX_LENGTH).replace(/-+$/, '')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The herdr name a coordinator takes when none is given: `jack-<repository>`. herdr names are unique across all
|
|
19
|
+
* workspaces, so two repositories cannot both run a coordinator called `jack`. The name fits
|
|
20
|
+
* `[a-z][a-z0-9_-]{0,31}`; a repository name that leaves nothing usable gives plain `jack`.
|
|
21
|
+
*/
|
|
22
|
+
export const defaultCoordinatorName = (repository: string): string =>
|
|
23
|
+
nameWith(COORDINATOR_NAME_PREFIX.current, repository)
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The coordinator's name among the agents' `names`: `jack-<repository>`, else the old `orchestrator-<repository>` of a
|
|
27
|
+
* session started before the rename, else `jack-<repository>` for none. The new name wins over the old one: a new
|
|
28
|
+
* start always takes it, so an agent carrying it is the newer coordinator, and the old one is a leftover.
|
|
29
|
+
*/
|
|
30
|
+
export const resolveCoordinatorName = (repository: string, names: readonly string[]): string => {
|
|
31
|
+
const current = defaultCoordinatorName(repository)
|
|
32
|
+
const legacy = nameWith(COORDINATOR_NAME_PREFIX.legacy, repository)
|
|
33
|
+
return names.includes(current) || !names.includes(legacy) ? current : legacy
|
|
16
34
|
}
|
package/src/context/index.ts
CHANGED
package/src/context/level.ts
CHANGED
|
@@ -2,21 +2,28 @@ export const CONTEXT_LEVEL = { ok: 'ok', warn: 'warn', alert: 'alert' } as const
|
|
|
2
2
|
|
|
3
3
|
export type ContextLevel = (typeof CONTEXT_LEVEL)[keyof typeof CONTEXT_LEVEL]
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/** Thresholds in tokens in the context, whatever the window. */
|
|
6
|
+
export type ContextThresholds = { warnTokens: number; alertTokens: number }
|
|
6
7
|
|
|
7
|
-
/** A fill as a view shows it: the share of the window and which threshold
|
|
8
|
+
/** A fill as a view shows it: the share of the window and which threshold its tokens have passed. */
|
|
8
9
|
export type ContextReading = { share: number; level: ContextLevel }
|
|
9
10
|
|
|
10
|
-
/**
|
|
11
|
-
export const
|
|
12
|
-
|
|
13
|
-
return share >= warnAt ? CONTEXT_LEVEL.warn : CONTEXT_LEVEL.ok
|
|
14
|
-
}
|
|
11
|
+
/** The highest share of the window a threshold may stand at, so a small window still hands over before it is full. */
|
|
12
|
+
export const WARN_SHARE_CAP = 0.6
|
|
13
|
+
export const ALERT_SHARE_CAP = 0.85
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
/**
|
|
16
|
+
* `ok` below `warnTokens`, `warn` from there, `alert` from `alertTokens` on. With a `window`, each threshold stops at
|
|
17
|
+
* 60 % or 85 % of it.
|
|
18
|
+
*/
|
|
19
|
+
export const contextLevel = (
|
|
20
|
+
tokens: number,
|
|
21
|
+
{ warnTokens, alertTokens }: ContextThresholds,
|
|
22
|
+
window = Number.POSITIVE_INFINITY,
|
|
23
|
+
): ContextLevel => {
|
|
24
|
+
if (tokens >= Math.min(alertTokens, window * ALERT_SHARE_CAP)) return CONTEXT_LEVEL.alert
|
|
25
|
+
return tokens >= Math.min(warnTokens, window * WARN_SHARE_CAP) ? CONTEXT_LEVEL.warn : CONTEXT_LEVEL.ok
|
|
26
|
+
}
|
|
20
27
|
|
|
21
28
|
/** `62 %` */
|
|
22
29
|
export const contextLabel = (share: number): string => `${Math.round(share * 100)} %`
|
package/src/context/report.ts
CHANGED
|
@@ -4,17 +4,20 @@ import { type ContextLevel, type ContextThresholds, contextLevel } from './level
|
|
|
4
4
|
|
|
5
5
|
export type ContextReport = ContextFill & { level: ContextLevel }
|
|
6
6
|
|
|
7
|
+
/** What a report reads its fill against: the windows and the thresholds. */
|
|
8
|
+
export type ReportSettings = ContextWindows & ContextThresholds
|
|
9
|
+
|
|
7
10
|
export type AgentLogReader = (agent: Pick<LaneAgent, 'session' | 'cwd'>) => string[] | null
|
|
8
11
|
|
|
9
12
|
/** The fill of one agent with its level; null without an agent, a log or an answer in it. */
|
|
10
13
|
export const contextOfAgent = (
|
|
11
14
|
agent: Pick<LaneAgent, 'session' | 'cwd'> | undefined,
|
|
12
15
|
readLog: AgentLogReader,
|
|
13
|
-
settings:
|
|
16
|
+
settings: ReportSettings,
|
|
14
17
|
): ContextReport | null => {
|
|
15
18
|
const lines = agent ? readLog(agent) : null
|
|
16
19
|
const fill = lines && contextFill(lines, settings)
|
|
17
|
-
return fill ? { ...fill, level: contextLevel(fill.
|
|
20
|
+
return fill ? { ...fill, level: contextLevel(fill.tokens, settings, fill.window) } : null
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
/**
|
|
@@ -25,7 +28,7 @@ export const contextReport = (
|
|
|
25
28
|
agents: readonly LaneAgent[],
|
|
26
29
|
lane: string,
|
|
27
30
|
readLog: AgentLogReader,
|
|
28
|
-
settings:
|
|
31
|
+
settings: ReportSettings,
|
|
29
32
|
): ContextReport | null =>
|
|
30
33
|
contextOfAgent(
|
|
31
34
|
agents.find((agent) => agent.name === lane),
|