@joshuafolkken/kit 1.1094.0 → 1.1136.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/.claude/agents/investigator.md +17 -0
- package/.claude/settings.json +1 -0
- package/.claude/skills/workflow-commands/SKILL.md +1 -1
- package/.claude/skills/workflow-commands/background-commands.md +36 -85
- package/.claude/skills/workflow-commands/backlogrun-progress.md +31 -34
- package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -2
- package/.claude/skills/workflow-commands/delegation.md +5 -1
- package/.claude/skills/workflow-commands/fullrun.md +5 -4
- package/.claude/skills/workflow-commands/halfrun.md +5 -4
- package/.claude/skills/workflow-commands/latest-gate.md +7 -15
- package/.claude/skills/workflow-commands/pre-gate-cut.md +4 -8
- package/.claude/skills/workflow-commands/split-assessment.md +4 -18
- package/.github/actions/setup-node/action.yml +33 -0
- package/.github/actions/setup-pnpm/action.yml +4 -4
- package/.github/workflows/auto-tag.yml +2 -3
- package/.github/workflows/ci.yml +7 -8
- package/.github/workflows/dependabot-auto-merge.yml +1 -0
- package/.github/workflows/pr-classification.yml +1 -3
- package/.github/workflows/production.yml +1 -0
- package/.github/workflows/publish.yml +10 -9
- package/.github/workflows/sonar-qube.yml +17 -13
- package/.pnpmfile.mjs +29 -0
- package/AGENTS.md +4 -11
- package/CLAUDE.md +24 -17
- package/GEMINI.md +4 -13
- package/README.md +8 -3
- package/SECURITY.md +2 -0
- package/cspell/index.yaml +4 -0
- package/dist/CLAUDE.basic.md +6 -6
- package/dist/CLAUDE.md +24 -17
- package/dist/CLAUDE.static.md +6 -6
- package/dist/config-merge/index.js +33 -9
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-OIZUHPJI.js → chunk-FO2I7UT7.js} +1 -1
- package/dist/hooks/{chunk-NKOG3XAV.js → chunk-GDU26K3J.js} +11 -3
- package/dist/hooks/{chunk-O4LL4HTI.js → chunk-HANSKHES.js} +2 -2
- package/dist/hooks/{chunk-3C4YCCEF.js → chunk-HBKZLITF.js} +4 -4
- package/dist/hooks/{chunk-UD24EAIL.js → chunk-JKKTVDAG.js} +1984 -146
- package/dist/hooks/{chunk-M2XDCUKD.js → chunk-KBXZLMRH.js} +89 -1803
- package/dist/hooks/{chunk-IL27YTPH.js → chunk-KOKOAV6D.js} +2 -2
- package/dist/hooks/{chunk-7ZPFAVR7.js → chunk-LJBLGHEH.js} +59 -19
- package/dist/hooks/{chunk-E54KGFEQ.js → chunk-NFZGFGOR.js} +1 -1
- package/dist/hooks/{chunk-KHWHKJOM.js → chunk-RFWWUI5F.js} +4 -4
- package/dist/hooks/codex-hook-adapter.js +12 -47
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +2 -2
- package/dist/hooks/inputs.json +175 -173
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +9 -9
- package/dist/hooks/stop-guard.js +17 -12
- package/dist/josh.js +87 -31
- package/dist/version/index.js +36 -20
- package/eslint/rules/unicorn.js +3 -2
- package/package.json +23 -5
- package/pnpm-workspace.yaml +5 -7
- package/prompts/collaboration-workflow/glossary.md +17 -0
- package/prompts/collaboration-workflow/issue-template.md +1 -1
- package/prompts/collaboration-workflow/operating-rules.md +1 -1
- package/prompts/collaboration-workflow/principles.md +9 -0
- package/prompts/collaboration-workflow.md +1 -0
- package/scripts/backlog/backlog-arrival.ts +30 -10
- package/scripts/backlog/backlog-budget-cli.ts +15 -16
- package/scripts/backlog/backlog-offer-cli.ts +6 -14
- package/scripts/backlog/backlog-plan.ts +29 -18
- package/scripts/backlog/backlog-ready.ts +20 -7
- package/scripts/config-merge/parse-jsonc.ts +33 -3
- package/scripts/config-merge/patch-json-key.ts +13 -6
- package/scripts/cost-runtime/cost-cli.ts +3 -7
- package/scripts/delegation/delegation-policy.ts +9 -1
- package/scripts/delegation/investigation-guard.ts +1 -1
- package/scripts/delegation/investigation-reads.ts +105 -19
- package/scripts/doctor/doctor-consumer.ts +3 -5
- package/scripts/doctor/doctor-io.ts +9 -13
- package/scripts/document/backlogrun-parent-read-set.ts +12 -1
- package/scripts/document/capped-print.ts +74 -0
- package/scripts/document/document-byte-budget.ts +1 -0
- package/scripts/document/document-section-cli.ts +4 -3
- package/scripts/document/entry-read-budget.ts +13 -7
- package/scripts/document/entry-read-set.ts +32 -8
- package/scripts/document/lane-child-read-set.ts +7 -0
- package/scripts/document/read-set-point-of-use.ts +8 -3
- package/scripts/document/read-set-trim.ts +3 -1
- package/scripts/epic/epic-audit.ts +3 -18
- package/scripts/gate/scoped-green.ts +25 -14
- package/scripts/gh/git-gh-issue-list.ts +12 -11
- package/scripts/gh/git-gh-issue-write.ts +4 -1
- package/scripts/gh/git-gh-pr-auto-merge.ts +4 -1
- package/scripts/git/git-command.ts +6 -1
- package/scripts/git/git-spawn-sync.ts +65 -0
- package/scripts/git/git-spawn.ts +5 -4
- package/scripts/git/stash/stash-pop-args.ts +1 -6
- package/scripts/init/claude-kit-only-settings.ts +29 -0
- package/scripts/init/init-copy-content.ts +8 -2
- package/scripts/init/init-file-action.ts +54 -0
- package/scripts/init/init-logic-migrate.ts +35 -17
- package/scripts/init/init-logic-secretlint.ts +1 -1
- package/scripts/init/init-logic.ts +6 -22
- package/scripts/init/init.ts +11 -68
- package/scripts/init/initial-commit.ts +24 -0
- package/scripts/init/kit-written-paths.ts +2 -0
- package/scripts/init/project-profile.ts +3 -6
- package/scripts/init/start-exec.ts +16 -3
- package/scripts/init/start-steps.ts +10 -13
- package/scripts/issue/defect-rate-cli.ts +61 -20
- package/scripts/issue/issue-backlinks.ts +7 -14
- package/scripts/issue/issue-cite.ts +12 -6
- package/scripts/issue/issue-file.ts +5 -12
- package/scripts/issue/issue-read-cli.ts +1 -0
- package/scripts/issue/issue-report-failures.ts +6 -2
- package/scripts/issue/issue-state-cli.ts +1 -1
- package/scripts/issue/markdown-section.ts +21 -6
- package/scripts/issue/measure-rerun-cli.ts +73 -14
- package/scripts/josh/josh-commands-lane.ts +1 -1
- package/scripts/josh/josh-commands-project.ts +8 -1
- package/scripts/josh/josh-commands-workflow.ts +1 -1
- package/scripts/josh/run-github-prerequisite.ts +3 -6
- package/scripts/lane/lane-await.ts +89 -15
- package/scripts/lane/lane-cli.ts +11 -13
- package/scripts/lane/lane-close.ts +4 -1
- package/scripts/lane/lane-launch-cli.ts +10 -16
- package/scripts/lane/openai-review-broker.ts +4 -1
- package/scripts/lib/cli-flags.ts +31 -2
- package/scripts/lib/error-message.ts +12 -1
- package/scripts/lines/line-targets.ts +0 -11
- package/scripts/lines/lines-command.ts +5 -3
- package/scripts/propagate/propagate-git.ts +3 -8
- package/scripts/refactor/refactor-scan.ts +2 -1
- package/scripts/registry-migration/migrate-logic.ts +28 -19
- package/scripts/registry-migration/migrate.ts +6 -5
- package/scripts/release/github-release-cli.ts +14 -3
- package/scripts/release/github-release-environment.ts +1 -0
- package/scripts/release/github-release.ts +103 -17
- package/scripts/release/release-publish.ts +36 -2
- package/scripts/release/release-worktree.ts +97 -2
- package/scripts/review/review-checkout.ts +3 -2
- package/scripts/review/review-record-cli.ts +9 -16
- package/scripts/rules/early-heartbeat.ts +4 -3
- package/scripts/rules/lane-park.ts +9 -0
- package/scripts/rules/stop-rules.ts +7 -4
- package/scripts/run/run-carry-args.ts +8 -12
- package/scripts/run/run-carry-cli.ts +3 -1
- package/scripts/run/run-carry-conversation.ts +94 -0
- package/scripts/run/run-carry-reclaim.ts +37 -0
- package/scripts/run/run-carry.ts +21 -5
- package/scripts/run/run-cut-args.ts +1 -6
- package/scripts/run/run-ending-cli.ts +1 -5
- package/scripts/run/run-entry-cli.ts +40 -9
- package/scripts/run/run-entry-stop.ts +104 -0
- package/scripts/run/run-event-stream-emit.ts +7 -2
- package/scripts/run/run-hold.ts +4 -1
- package/scripts/run/run-liveness-cli.ts +1 -5
- package/scripts/run/run-merge-cli.ts +2 -10
- package/scripts/run/run-merge-steps.ts +4 -1
- package/scripts/run/run-parent-cut-hook.ts +4 -1
- package/scripts/run/run-prep-cli.ts +4 -1
- package/scripts/run/run-progress-args.ts +20 -4
- package/scripts/run/run-progress-cli.ts +31 -29
- package/scripts/run/run-progress-clock.ts +4 -1
- package/scripts/run/run-ship-cli.ts +8 -16
- package/scripts/run/run-ship-probe.ts +4 -1
- package/scripts/run/run-status-cli.ts +2 -15
- package/scripts/run/run-tidy-lanes.ts +4 -1
- package/scripts/run/run-wake-cli.ts +2 -10
- package/scripts/run/run-watcher-guard.ts +9 -5
- package/scripts/safe-chain/pack-hook.ts +92 -0
- package/scripts/sync/sync-ai-files.ts +4 -0
- package/scripts/test/test-declared-changed.ts +13 -16
- package/scripts/test/test-declared.ts +3 -7
- package/scripts/test/test-related.ts +1 -6
- package/scripts/test/test-unit-guard.ts +21 -7
- package/scripts/time-runtime/time-spans.ts +4 -0
- package/scripts/version/version-commands.ts +11 -1
- package/templates/CLAUDE.basic.md +6 -6
- package/templates/workflows/ci.yml +16 -82
- package/templates/workflows/dependabot-auto-merge.yml +1 -0
- package/templates/workflows/github-release.yml +14 -28
- package/tsconfig.sonar.json +4 -22
|
@@ -32,11 +32,7 @@ type ParsedValues = Partial<Record<OptionName, string | boolean>>
|
|
|
32
32
|
// A misspelled flag is refused rather than defaulted, the contract `path_decision.has_unknown_flag`
|
|
33
33
|
// states for the path-decided commands: an invocation nobody can read is never answered.
|
|
34
34
|
function read_arguments(argv: ReadonlyArray<string>): ParsedValues | undefined {
|
|
35
|
-
return cli_flags.
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function text_of(value: string | boolean | undefined): string | undefined {
|
|
39
|
-
return typeof value === 'string' ? value : undefined
|
|
35
|
+
return cli_flags.values_of(argv, OPTIONS)
|
|
40
36
|
}
|
|
41
37
|
|
|
42
38
|
// `find` rather than a cast: an unrecognized word has to be refused, and a type assertion would
|
|
@@ -79,15 +75,15 @@ type Counts = Record<(typeof COUNT_NAMES)[number], number | undefined>
|
|
|
79
75
|
|
|
80
76
|
function counts_of(values: ParsedValues): Counts {
|
|
81
77
|
return {
|
|
82
|
-
idle: to_count(
|
|
83
|
-
max: to_count(
|
|
84
|
-
merged: to_count(
|
|
85
|
-
running: to_count(
|
|
78
|
+
idle: to_count(cli_flags.string_of(values.idle)),
|
|
79
|
+
max: to_count(cli_flags.string_of(values.max)),
|
|
80
|
+
merged: to_count(cli_flags.string_of(values.merged)),
|
|
81
|
+
running: to_count(cli_flags.string_of(values.running)),
|
|
86
82
|
}
|
|
87
83
|
}
|
|
88
84
|
|
|
89
85
|
function are_counts_readable(values: ParsedValues, counts: Counts): boolean {
|
|
90
|
-
return COUNT_NAMES.every((name) => is_readable(
|
|
86
|
+
return COUNT_NAMES.every((name) => is_readable(cli_flags.string_of(values[name]), counts[name]))
|
|
91
87
|
}
|
|
92
88
|
|
|
93
89
|
// **`--idle 0` is how the watch is turned off, and omitting the flag takes the default**
|
|
@@ -116,14 +112,17 @@ function to_idle_ms(idle_minutes: number | undefined): number | undefined {
|
|
|
116
112
|
// lands on the very first ask, before the run has started anything, and the fix is one flag the loop
|
|
117
113
|
// already holds: it always knows when it last had work.
|
|
118
114
|
function is_watch_readable(values: ParsedValues): boolean {
|
|
119
|
-
if (to_idle_ms(to_count(
|
|
115
|
+
if (to_idle_ms(to_count(cli_flags.string_of(values.idle))) === undefined) return true
|
|
120
116
|
|
|
121
|
-
return
|
|
117
|
+
return cli_flags.string_of(values.active) !== undefined
|
|
122
118
|
}
|
|
123
119
|
|
|
124
120
|
function are_values_readable(values: ParsedValues): boolean {
|
|
125
121
|
return (
|
|
126
|
-
is_readable(
|
|
122
|
+
is_readable(
|
|
123
|
+
cli_flags.string_of(values.active),
|
|
124
|
+
to_time_ms(cli_flags.string_of(values.active)),
|
|
125
|
+
) &&
|
|
127
126
|
is_watch_readable(values) &&
|
|
128
127
|
are_counts_readable(values, counts_of(values))
|
|
129
128
|
)
|
|
@@ -138,7 +137,7 @@ function input_of(values: ParsedValues, base: InputBase): BudgetInput {
|
|
|
138
137
|
|
|
139
138
|
return {
|
|
140
139
|
...base,
|
|
141
|
-
active_at_ms: to_time_ms(
|
|
140
|
+
active_at_ms: to_time_ms(cli_flags.string_of(values.active)) ?? base.started_at_ms,
|
|
142
141
|
merged: counts.merged ?? 0,
|
|
143
142
|
running: counts.running ?? 0,
|
|
144
143
|
max_issues: counts.max,
|
|
@@ -147,11 +146,11 @@ function input_of(values: ParsedValues, base: InputBase): BudgetInput {
|
|
|
147
146
|
}
|
|
148
147
|
|
|
149
148
|
function build_input(values: ParsedValues, now_ms: number): BudgetInput | undefined {
|
|
150
|
-
const answer = to_answer(
|
|
149
|
+
const answer = to_answer(cli_flags.string_of(values.answer))
|
|
151
150
|
|
|
152
151
|
if (answer === undefined) return undefined
|
|
153
152
|
|
|
154
|
-
const started_at_ms = to_time_ms(
|
|
153
|
+
const started_at_ms = to_time_ms(cli_flags.string_of(values.started))
|
|
155
154
|
|
|
156
155
|
if (started_at_ms === undefined) return undefined
|
|
157
156
|
if (!are_values_readable(values)) return undefined
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
-
import { parseArgs } from 'node:util'
|
|
4
3
|
import { josh_command } from '#scripts/josh/josh-run'
|
|
4
|
+
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
5
5
|
import { rule_value_cache } from '#scripts/rules/rule-value-cache'
|
|
6
6
|
import { run_event_stream } from '#scripts/run/run-event-stream'
|
|
7
7
|
import { run_event_stream_emit } from '#scripts/run/run-event-stream-emit'
|
|
@@ -61,15 +61,7 @@ type ParsedValues = Partial<Record<OptionName, string | boolean | ReadonlyArray<
|
|
|
61
61
|
const FORWARDED = ['started', 'active', 'merged', 'running', 'max', 'idle'] as const
|
|
62
62
|
|
|
63
63
|
function read_values(argv: ReadonlyArray<string>): ParsedValues | undefined {
|
|
64
|
-
|
|
65
|
-
return parseArgs({ args: [...argv], options: OPTIONS, strict: true }).values
|
|
66
|
-
} catch {
|
|
67
|
-
return undefined
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function text_of(value: string | boolean | ReadonlyArray<string> | undefined): string | undefined {
|
|
72
|
-
return typeof value === 'string' ? value : undefined
|
|
64
|
+
return cli_flags.values_of(argv, OPTIONS)
|
|
73
65
|
}
|
|
74
66
|
|
|
75
67
|
function to_count(raw: string | undefined): number | undefined {
|
|
@@ -94,8 +86,8 @@ interface OfferCounts {
|
|
|
94
86
|
// The two counts the mapping needs: `--running` decides `wait`, `--retries` decides `retry`. Either
|
|
95
87
|
// given but unreadable refuses the whole invocation.
|
|
96
88
|
function counts_of(values: ParsedValues): OfferCounts | undefined {
|
|
97
|
-
const running = count_of(
|
|
98
|
-
const retries = count_of(
|
|
89
|
+
const running = count_of(cli_flags.string_of(values.running), DEFAULT_COUNT)
|
|
90
|
+
const retries = count_of(cli_flags.string_of(values.retries), DEFAULT_COUNT)
|
|
99
91
|
|
|
100
92
|
if (running === undefined || retries === undefined) return undefined
|
|
101
93
|
|
|
@@ -109,7 +101,7 @@ function excludes_of(values: ParsedValues): ReadonlyArray<string> {
|
|
|
109
101
|
}
|
|
110
102
|
|
|
111
103
|
function next_argv(values: ParsedValues): ReadonlyArray<string> {
|
|
112
|
-
const repo =
|
|
104
|
+
const repo = cli_flags.string_of(values.repo)
|
|
113
105
|
|
|
114
106
|
return [
|
|
115
107
|
'backlog:next',
|
|
@@ -120,7 +112,7 @@ function next_argv(values: ParsedValues): ReadonlyArray<string> {
|
|
|
120
112
|
|
|
121
113
|
function budget_argv(values: ParsedValues, answer: string): ReadonlyArray<string> {
|
|
122
114
|
const forwarded = FORWARDED.flatMap((name) => {
|
|
123
|
-
const value =
|
|
115
|
+
const value = cli_flags.string_of(values[name])
|
|
124
116
|
|
|
125
117
|
return value === undefined ? [] : [`--${name}`, value]
|
|
126
118
|
})
|
|
@@ -3,6 +3,7 @@ import type { IssueReference } from '#scripts/epic/epic-reference'
|
|
|
3
3
|
import { epic_report, type EpicNextResult } from '#scripts/epic/epic-report'
|
|
4
4
|
import { epic_solo } from '#scripts/epic/epic-solo'
|
|
5
5
|
import { epic_triage } from '#scripts/epic/epic-triage'
|
|
6
|
+
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
6
7
|
import { has_label_name, IN_PROGRESS_LABEL, RUN_SOLO_LABEL } from '#scripts/issue/issue-labels'
|
|
7
8
|
import type { OutOfScopeRow } from './backlog-scope'
|
|
8
9
|
|
|
@@ -57,23 +58,29 @@ interface PlanContext {
|
|
|
57
58
|
|
|
58
59
|
// A child elsewhere is qualified, because a bare number would name *this* repository's issue of that
|
|
59
60
|
// number — a different issue (joshuafolkken/kit#1016). Takes the reference rather than the child, so
|
|
60
|
-
// a `blocked_by` edge is named by the same rule the child itself is.
|
|
61
|
-
|
|
61
|
+
// a `blocked_by` edge is named by the same rule the child itself is. This repository's issue is cited
|
|
62
|
+
// (joshuafolkken/kit#3099): the plan is copied into a report verbatim, so the line it prints is the
|
|
63
|
+
// citation the report carries.
|
|
64
|
+
function cite_of(reference: IssueReference, repo: string, title: string | undefined): string {
|
|
62
65
|
const number = String(reference.number)
|
|
63
66
|
|
|
64
|
-
return reference.repo === repo
|
|
67
|
+
return reference.repo === repo
|
|
68
|
+
? issue_cite.reference(repo, number, title)
|
|
69
|
+
: `${reference.repo}#${number}`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function reference_of(reference: IssueReference, repo: string): string {
|
|
73
|
+
return cite_of(reference, repo, undefined)
|
|
65
74
|
}
|
|
66
75
|
|
|
67
76
|
// A title is only known for this repository's issues — the open listing the titles come from is this
|
|
68
77
|
// repository's. A child elsewhere is still named, just without one.
|
|
69
|
-
function title_of(child: EpicChild, context: PlanContext): string {
|
|
70
|
-
return child.repo === context.repo ?
|
|
78
|
+
function title_of(child: EpicChild, context: PlanContext): string | undefined {
|
|
79
|
+
return child.repo === context.repo ? context.titles.get(child.number) : undefined
|
|
71
80
|
}
|
|
72
81
|
|
|
73
|
-
function join_row(reference: string,
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return `${ROW_INDENT}${parts.join(' ')}`
|
|
82
|
+
function join_row(reference: string, note: string): string {
|
|
83
|
+
return note === '' ? `${ROW_INDENT}${reference}` : `${ROW_INDENT}${reference} — ${note}`
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
// A `run:solo` child is marked beside its number, so a person reading the plan sees which issues
|
|
@@ -89,15 +96,15 @@ function mark_of(child: EpicChild): string | undefined {
|
|
|
89
96
|
return epic_triage.is_triaged(child) ? undefined : UNTRIAGED_MARK
|
|
90
97
|
}
|
|
91
98
|
|
|
92
|
-
function marked_reference(child: EpicChild, repo: string): string {
|
|
93
|
-
const reference =
|
|
99
|
+
function marked_reference(child: EpicChild, repo: string, title?: string): string {
|
|
100
|
+
const reference = cite_of(child, repo, title)
|
|
94
101
|
const mark = mark_of(child)
|
|
95
102
|
|
|
96
103
|
return mark === undefined ? reference : `${reference} ${mark}`
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
function row_of(child: EpicChild, context: PlanContext, note: string): string {
|
|
100
|
-
return join_row(marked_reference(child, context.repo
|
|
107
|
+
return join_row(marked_reference(child, context.repo, title_of(child, context)), note)
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
// Why this one is not offered yet. The blocker numbers are the answer whenever there are any — that
|
|
@@ -164,8 +171,10 @@ function waiting_lines(children: ReadonlyArray<EpicChild>, context: PlanContext)
|
|
|
164
171
|
})
|
|
165
172
|
}
|
|
166
173
|
|
|
167
|
-
function scope_lines(rows: ReadonlyArray<OutOfScopeRow
|
|
168
|
-
return rows.map((row) =>
|
|
174
|
+
function scope_lines(rows: ReadonlyArray<OutOfScopeRow>, repo: string): Array<string> {
|
|
175
|
+
return rows.map((row) =>
|
|
176
|
+
join_row(issue_cite.reference(repo, String(row.number), row.title), row.reason),
|
|
177
|
+
)
|
|
169
178
|
}
|
|
170
179
|
|
|
171
180
|
// The classification could not be made, so every section below it would be rendered from buckets
|
|
@@ -186,10 +195,12 @@ interface NamedPlan {
|
|
|
186
195
|
only: boolean
|
|
187
196
|
}
|
|
188
197
|
|
|
189
|
-
// The named issues are this repository's own, so
|
|
190
|
-
// same way the pool's rows are. The order is the invocation's, never sorted.
|
|
198
|
+
// The named issues are this repository's own, so they are cited against it and a title is looked up
|
|
199
|
+
// the same way the pool's rows are. The order is the invocation's, never sorted.
|
|
191
200
|
function named_lines(named: ReadonlyArray<number>, context: PlanContext): Array<string> {
|
|
192
|
-
return named.map((issue) =>
|
|
201
|
+
return named.map((issue) =>
|
|
202
|
+
join_row(issue_cite.reference(context.repo, String(issue), context.titles.get(issue)), ''),
|
|
203
|
+
)
|
|
193
204
|
}
|
|
194
205
|
|
|
195
206
|
// The section leads the plan when the invocation named issues, and is absent otherwise — a bare
|
|
@@ -230,7 +241,7 @@ function format_plan(
|
|
|
230
241
|
'',
|
|
231
242
|
section(HUMAN_HEADING, child_lines(result.blocked_on_people, context, no_note)),
|
|
232
243
|
'',
|
|
233
|
-
section(SCOPE_HEADING, scope_lines(out_of_scope)),
|
|
244
|
+
section(SCOPE_HEADING, scope_lines(out_of_scope, context.repo)),
|
|
234
245
|
].join('\n')
|
|
235
246
|
}
|
|
236
247
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { git_gh_command } from '#scripts/gh/git-gh-command'
|
|
2
|
+
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
1
3
|
import { josh_command, type JoshResult } from '#scripts/josh/josh-run'
|
|
2
4
|
import { lane_capacity } from '#scripts/lane/lane-capacity'
|
|
3
5
|
import { lane_registry } from '#scripts/lane/lane-registry'
|
|
@@ -92,35 +94,46 @@ async function read_ready(ports: ReadyPorts = DEFAULT_PORTS): Promise<ReadyReadi
|
|
|
92
94
|
return { issues: await ports.ready_issues(), free_lanes }
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
// `undefined` unless there is both runnable work and a lane for it — the only state worth a line.
|
|
96
|
-
|
|
97
|
+
// `undefined` unless there is both runnable work and a lane for it — the only state worth a line. Each
|
|
98
|
+
// issue is a number-link against `slug` (joshuafolkken/kit#3099): the parent copies this line into its
|
|
99
|
+
// report, so a bare `#N` here is the citation the Stop guard sends back.
|
|
100
|
+
function ready_line(reading: ReadyReading, slug: string | undefined): string | undefined {
|
|
97
101
|
if (reading.issues.length === NONE || reading.free_lanes <= NO_FREE) return undefined
|
|
98
102
|
|
|
99
|
-
const
|
|
103
|
+
const cite = issue_cite.citer(slug, new Map())
|
|
104
|
+
const issues = reading.issues.map((issue) => cite(issue)).join(' ')
|
|
100
105
|
|
|
101
106
|
return `ready ${issues} · free lanes ${String(reading.free_lanes)}`
|
|
102
107
|
}
|
|
103
108
|
|
|
109
|
+
type RepoRead = () => Promise<string | undefined>
|
|
110
|
+
|
|
104
111
|
// The watcher's line on exit, for the driving parent alone — a `fullrun` watcher has no pool to pick
|
|
105
112
|
// from. Best-effort: a reading that fails prints nothing rather than failing the watcher whose exit is
|
|
106
113
|
// the wake.
|
|
107
114
|
async function parent_ready_line(
|
|
108
115
|
ports: ReadyPorts,
|
|
109
116
|
is_parent: () => Promise<boolean>,
|
|
117
|
+
read_repo: RepoRead,
|
|
110
118
|
): Promise<string | undefined> {
|
|
111
119
|
if (!(await is_parent())) return undefined
|
|
112
120
|
|
|
113
|
-
|
|
121
|
+
const reading = await read_ready(ports)
|
|
122
|
+
|
|
123
|
+
return reading.issues.length === NONE ? undefined : ready_line(reading, await read_repo())
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function print_line(line: string | undefined): void {
|
|
127
|
+
if (line !== undefined) console.info(line)
|
|
114
128
|
}
|
|
115
129
|
|
|
116
130
|
async function print_ready_line(
|
|
117
131
|
ports: ReadyPorts = DEFAULT_PORTS,
|
|
118
132
|
is_parent: () => Promise<boolean> = run_headless.is_backlog_parent,
|
|
133
|
+
read_repo: RepoRead = git_gh_command.repo_get_name_with_owner,
|
|
119
134
|
): Promise<void> {
|
|
120
135
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (line !== undefined) console.info(line)
|
|
136
|
+
print_line(await parent_ready_line(ports, is_parent, read_repo))
|
|
124
137
|
} catch {
|
|
125
138
|
// A pick-up line is a hint; the wake it rides must still be delivered.
|
|
126
139
|
}
|
|
@@ -1,14 +1,44 @@
|
|
|
1
1
|
import { json_object_schema } from '#scripts/lib/schemas'
|
|
2
|
-
import
|
|
2
|
+
import { parseTree, printParseErrorCode, type Node, type ParseError } from 'jsonc-parser'
|
|
3
3
|
|
|
4
4
|
// Every managed JSON config kit reads (tsconfig.json, package.json, .vscode/*.json) may legally
|
|
5
5
|
// carry comments and trailing commas — TypeScript and VS Code both parse them as JSONC — so a plain
|
|
6
6
|
// `JSON.parse` would throw on files consumers hand-authored. Single-sourced here because the
|
|
7
7
|
// config-merge library and the init/sync merge helpers both need the exact same tolerant read.
|
|
8
|
+
|
|
9
|
+
// Built from the tree rather than with jsonc-parser's own `parse` / `getNodeValue`, which assign each
|
|
10
|
+
// key: a `"__proto__"` key would become the object's prototype instead of the own property
|
|
11
|
+
// `JSON.parse` keeps, and the spread-and-stringify write-back would then drop it from the file.
|
|
12
|
+
// `Object.fromEntries` defines properties, so every key stays data.
|
|
13
|
+
function property_key(property: Node): string {
|
|
14
|
+
return String(property.children?.[0]?.value)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function node_value(node: Node | undefined): unknown {
|
|
18
|
+
if (node === undefined) return undefined
|
|
19
|
+
const children = node.children ?? []
|
|
20
|
+
if (node.type === 'array') return children.map((child) => node_value(child))
|
|
21
|
+
if (node.type !== 'object') return node.value
|
|
22
|
+
|
|
23
|
+
return Object.fromEntries(
|
|
24
|
+
children.map((property) => [property_key(property), node_value(property.children?.[1])]),
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// jsonc-parser recovers from errors and returns a best-effort tree, so any reported error is
|
|
29
|
+
// rethrown: a malformed document must fail the read the way `JSON.parse` would, not merge partially.
|
|
8
30
|
function parse_jsonc(content: string): Record<string, unknown> {
|
|
9
|
-
const
|
|
31
|
+
const errors: Array<ParseError> = []
|
|
32
|
+
const tree = parseTree(content, errors, { allowTrailingComma: true })
|
|
33
|
+
const [first_error] = errors
|
|
34
|
+
|
|
35
|
+
if (first_error !== undefined) {
|
|
36
|
+
throw new SyntaxError(
|
|
37
|
+
`Invalid JSONC: ${printParseErrorCode(first_error.error)} at offset ${String(first_error.offset)}`,
|
|
38
|
+
)
|
|
39
|
+
}
|
|
10
40
|
|
|
11
|
-
return json_object_schema.parse(
|
|
41
|
+
return json_object_schema.parse(node_value(tree))
|
|
12
42
|
}
|
|
13
43
|
|
|
14
44
|
export { parse_jsonc }
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
createScanner,
|
|
3
|
+
findNodeAtLocation,
|
|
4
|
+
getNodeValue,
|
|
5
|
+
parseTree,
|
|
6
|
+
stripComments,
|
|
7
|
+
type Node,
|
|
8
|
+
} from 'jsonc-parser'
|
|
3
9
|
import { json_format } from './json-format'
|
|
4
10
|
|
|
5
11
|
// Set or remove one top-level key while leaving every other byte of the document alone.
|
|
@@ -163,13 +169,14 @@ function target_value_end(property: Node | undefined): number | undefined {
|
|
|
163
169
|
return value_node === undefined ? undefined : value_node.offset + value_node.length
|
|
164
170
|
}
|
|
165
171
|
|
|
166
|
-
// Whether the span holds a comment. `
|
|
167
|
-
//
|
|
168
|
-
// need `SyntaxKind`,
|
|
172
|
+
// Whether the span holds a comment. `stripComments` drops every comment token the scanner finds, so
|
|
173
|
+
// it changes the text exactly when a comment is present, and a `//` inside a string value is not
|
|
174
|
+
// counted — comparing token kinds directly would need `SyntaxKind`, an ambient const enum this
|
|
175
|
+
// project cannot import.
|
|
169
176
|
function contains_comment(content: string, node: Node): boolean {
|
|
170
177
|
const text = content.slice(node.offset, node.offset + node.length)
|
|
171
178
|
|
|
172
|
-
return
|
|
179
|
+
return stripComments(text) !== text
|
|
173
180
|
}
|
|
174
181
|
|
|
175
182
|
// Re-lay-out a property whose trailing comma has just appeared or disappeared. That one column
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
-
import { parseArgs } from 'node:util'
|
|
4
3
|
import { agent_role_profile, type AgentProvider } from '#scripts/agent/agent-role-profile'
|
|
4
|
+
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
5
5
|
import { codex_usage } from './codex-usage'
|
|
6
6
|
import { CONTEXT_CUT_THRESHOLD } from './context-cut-threshold'
|
|
7
7
|
import { cost_corpus } from './cost-corpus'
|
|
@@ -103,13 +103,9 @@ const PARSE_ARGS_OPTIONS = {
|
|
|
103
103
|
} as const
|
|
104
104
|
|
|
105
105
|
function parse_options(argv: ReadonlyArray<string>): Options | undefined {
|
|
106
|
-
|
|
107
|
-
const { values } = parseArgs({ args: [...argv], options: PARSE_ARGS_OPTIONS, strict: true })
|
|
106
|
+
const values = cli_flags.values_of(argv, PARSE_ARGS_OPTIONS)
|
|
108
107
|
|
|
109
|
-
|
|
110
|
-
} catch {
|
|
111
|
-
return undefined
|
|
112
|
-
}
|
|
108
|
+
return values === undefined ? undefined : to_options(values)
|
|
113
109
|
}
|
|
114
110
|
|
|
115
111
|
// An empty corpus is reported, never priced at zero. "No transcript was found" and "this run was
|
|
@@ -32,6 +32,13 @@ interface DelegatableStep {
|
|
|
32
32
|
// wait per turn — and below the threshold that round trip costs more than carrying the text.
|
|
33
33
|
const INVESTIGATION_FILE_THRESHOLD = 3
|
|
34
34
|
|
|
35
|
+
// The same boundary for searches, counted in turns (joshuafolkken/kit#3139). A `grep` carries no file
|
|
36
|
+
// text, so it never reached the file count — yet across 100 lane transcripts on 2026-10-04, 424 turns
|
|
37
|
+
// were searches alone and 160 of them came third or later since the last edit or delegation, each
|
|
38
|
+
// re-reading the whole main-line context. Three keeps the first two searches, which locate an edit
|
|
39
|
+
// target, in the main line and sends the continuing sweep to a unit.
|
|
40
|
+
const INVESTIGATION_SEARCH_TURN_THRESHOLD = 3
|
|
41
|
+
|
|
35
42
|
// The enumeration. Short on purpose: every entry had to name a verifier, and most candidates could
|
|
36
43
|
// not (below).
|
|
37
44
|
const DELEGATABLE_STEPS: ReadonlyArray<DelegatableStep> = [
|
|
@@ -88,7 +95,7 @@ const DELEGATABLE_STEPS: ReadonlyArray<DelegatableStep> = [
|
|
|
88
95
|
// verifiers differ, so widening `survey` would put both in one field and lose the boundary below.
|
|
89
96
|
// **No ordinal.** `${N}rd` would print `1rd` for any other threshold, past a test that only looks
|
|
90
97
|
// for the numeral, so the count is phrased as a plain "N or more".
|
|
91
|
-
does: `read the Issue's subject to find out how it currently works — once ${String(INVESTIGATION_FILE_THRESHOLD)} or more files the run will not edit have to be read — and return the conclusion plus the \`file:line\` citations that support it, never the file text, which would put the cost back where it was. A throwaway probe script is written, run and deleted inside the unit, which returns its output alone`,
|
|
98
|
+
does: `read the Issue's subject to find out how it currently works — once ${String(INVESTIGATION_FILE_THRESHOLD)} or more files the run will not edit have to be read, or ${String(INVESTIGATION_SEARCH_TURN_THRESHOLD)} or more search turns run without an edit — and return the conclusion plus the \`file:line\` citations that support it, never the file text, which would put the cost back where it was. A throwaway probe script is written, run and deleted inside the unit, which returns its output alone`,
|
|
92
99
|
verifier:
|
|
93
100
|
'the parent opens the cited lines; a conclusion those lines do not support fails there, and reading a handful of cited regions costs far less than redoing the reading. The unit reports what the code does and never what to change — deciding that is `diagnosis`, which stays kept, so a unit returning a root cause would be delegating the rejected row under this name',
|
|
94
101
|
},
|
|
@@ -176,6 +183,7 @@ function reason_for(name: string): string {
|
|
|
176
183
|
const delegation_policy = {
|
|
177
184
|
DELEGATE_VERDICT,
|
|
178
185
|
INVESTIGATION_FILE_THRESHOLD,
|
|
186
|
+
INVESTIGATION_SEARCH_TURN_THRESHOLD,
|
|
179
187
|
KEEP_VERDICT,
|
|
180
188
|
DELEGATABLE_STEPS,
|
|
181
189
|
REJECTED_STEPS,
|
|
@@ -72,7 +72,7 @@ function should_notify(
|
|
|
72
72
|
// (joshuafolkken/kit#2138, joshuafolkken/kit#2382). A dispatched lane child ends its turn on a denial, so
|
|
73
73
|
// the same guidance is carried without a `permissionDecision`: the read proceeds and the child is nudged
|
|
74
74
|
// to delegate rather than killed. It carries no ⛔, so a person watching reads it as advice.
|
|
75
|
-
const NOTICE = `💡 investigation: files read and not edited since the last delegated unit reached the threshold, so the reading from here should go to a unit of its own. This is a notice, not a refusal — the read proceeds, because a dispatched lane child ends its turn on a denial. Ask \`pnpm josh delegate investigation\`, then
|
|
75
|
+
const NOTICE = `💡 investigation: files read and not edited, or search turns, since the last delegated unit reached the threshold, so the reading from here should go to a unit of its own. This is a notice, not a refusal — the read proceeds, because a dispatched lane child ends its turn on a denial. Ask \`pnpm josh delegate investigation\`, then dispatch the \`${investigation_reads.INVESTIGATOR_AGENT}\` agent (\`kit:${investigation_reads.INVESTIGATOR_AGENT}\` in a consumer) with a brief of what the main line has already concluded and what is left to find out; it returns the conclusion plus its \`file:line\` citations, never the file text. Keep a read in the main line only where this run will edit that file. This recurs on the next accumulation.`
|
|
76
76
|
|
|
77
77
|
// The notice as it reaches the model: the base guidance with the concrete unedited reads named (kit#2276,
|
|
78
78
|
// kit#2382), so a lane child is shown *which* reads to send to a unit rather than only told to delegate —
|