@joshuafolkken/kit 1.1136.0 → 1.1178.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/settings.json +4 -4
- package/.claude/skills/epic-commands/SKILL.md +27 -239
- package/.claude/skills/epic-commands/epic-bundle.md +115 -0
- package/.claude/skills/epic-commands/execution-waves.md +126 -0
- package/.claude/skills/workflow-commands/SKILL.md +48 -194
- package/.claude/skills/workflow-commands/background-commands.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-child.md +12 -123
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +14 -63
- package/.claude/skills/workflow-commands/backlogrun-park.md +16 -15
- package/.claude/skills/workflow-commands/backlogrun-progress.md +17 -173
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +155 -0
- package/.claude/skills/workflow-commands/backlogrun-steps.md +65 -144
- package/.claude/skills/workflow-commands/backlogrun.md +5 -4
- package/.claude/skills/workflow-commands/chain-rule.md +3 -2
- package/.claude/skills/workflow-commands/delegation.md +2 -2
- package/.claude/skills/workflow-commands/entry-sequence.md +62 -0
- package/.claude/skills/workflow-commands/followup.md +104 -181
- package/.claude/skills/workflow-commands/fullrun-steps.md +8 -9
- package/.claude/skills/workflow-commands/fullrun.md +16 -73
- package/.claude/skills/workflow-commands/halfrun.md +10 -36
- package/.claude/skills/workflow-commands/into-target.md +4 -4
- package/.claude/skills/workflow-commands/issue-comments.md +7 -8
- package/.claude/skills/workflow-commands/issue-scout.md +6 -6
- package/.claude/skills/workflow-commands/kickoff.md +6 -6
- package/.claude/skills/workflow-commands/needs-human-review.md +6 -6
- package/.claude/skills/workflow-commands/observation-filing.md +21 -172
- package/.claude/skills/workflow-commands/observation-ledger.md +150 -0
- package/.claude/skills/workflow-commands/pre-gate-cut.md +35 -95
- package/.claude/skills/workflow-commands/prerequisite.md +6 -6
- package/.claude/skills/workflow-commands/progress-watcher.md +90 -0
- package/.claude/skills/workflow-commands/prrun.md +5 -5
- package/.claude/skills/workflow-commands/retrospective.md +9 -9
- package/.claude/skills/workflow-commands/split-assessment.md +2 -2
- package/.claude/skills/workflow-commands/target-repository.md +2 -2
- package/.claude/skills/workflow-commands/working-tree-hold.md +7 -5
- package/.codex/hooks.json +4 -4
- package/.github/workflows/ci.yml +15 -2
- package/AGENTS.md +1 -6
- package/CLAUDE.md +58 -120
- package/GEMINI.md +1 -6
- package/dist/CLAUDE.md +58 -120
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-NFZGFGOR.js → chunk-32GQTL4F.js} +14 -1
- package/dist/hooks/{chunk-HBKZLITF.js → chunk-DO6NWWHU.js} +4 -4
- package/dist/hooks/{chunk-KBXZLMRH.js → chunk-G6KRCKEC.js} +216 -76
- package/dist/hooks/{chunk-JKKTVDAG.js → chunk-GDS5N2KB.js} +186 -309
- package/dist/hooks/chunk-I4XSL4TT.js +56 -0
- package/dist/hooks/{chunk-GDU26K3J.js → chunk-S6X6WYKR.js} +886 -174
- package/dist/hooks/{chunk-FO2I7UT7.js → chunk-UAGLLIGZ.js} +1 -1
- package/dist/hooks/{chunk-RFWWUI5F.js → chunk-VESKRQOF.js} +3 -3
- package/dist/hooks/{chunk-KOKOAV6D.js → chunk-VQLLNFNT.js} +6 -4
- package/dist/hooks/{chunk-LJBLGHEH.js → chunk-ZAL3NRGL.js} +4 -6
- package/dist/hooks/codex-hook-adapter.js +10 -10
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +2 -2
- package/dist/hooks/inputs.json +178 -177
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +11 -10
- package/dist/hooks/stop-guard.js +20 -6
- package/dist/josh.js +29 -3
- package/dist/version/index.js +5 -1
- package/package.json +2 -1
- package/prompts/collaboration-workflow/file-edits.md +11 -10
- package/prompts/collaboration-workflow/gh-rest.md +2 -4
- package/prompts/collaboration-workflow/glossary.md +1 -1
- package/prompts/collaboration-workflow/issue-citation.md +5 -9
- package/prompts/collaboration-workflow/issue-template.md +40 -115
- package/prompts/collaboration-workflow/operating-rules.md +4 -5
- package/prompts/collaboration-workflow/output-bounds.md +12 -78
- package/prompts/collaboration-workflow/overview.md +7 -13
- package/prompts/collaboration-workflow/plan-comment.md +11 -39
- package/prompts/collaboration-workflow/principles.md +3 -4
- package/prompts/collaboration-workflow/report-format.md +28 -74
- package/prompts/collaboration-workflow/residency.md +12 -109
- package/prompts/collaboration-workflow/rule-delivery.md +98 -97
- package/prompts/collaboration-workflow/shell-body.md +3 -13
- package/prompts/collaboration-workflow/turn-batching.md +14 -88
- package/prompts/collaboration-workflow/upstream-interrupt.md +12 -14
- package/prompts/collaboration-workflow/wip-cap.md +6 -45
- package/prompts/collaboration-workflow.md +1 -13
- package/prompts/review-rubric.md +8 -71
- package/prompts/review.md +25 -55
- package/prompts/testing-guide.md +18 -67
- package/scripts/agent/agent-role-profile.ts +1 -1
- package/scripts/agent/codex-hooks.ts +3 -12
- package/scripts/backlog/backlog-budget.ts +1 -1
- package/scripts/backlog/backlog-drive-cli.ts +4 -3
- package/scripts/backlog/backlog-drive-offer-argv.ts +10 -1
- package/scripts/backlog/backlog-next.ts +15 -4
- package/scripts/backlog/backlog-offer.ts +6 -6
- package/scripts/backlog/backlog-plan-cli.ts +6 -5
- package/scripts/backlog/backlog-rank.ts +18 -9
- package/scripts/backlog/backlog-restructure.ts +152 -0
- package/scripts/backlog/backlog-waves.ts +15 -17
- package/scripts/behavior/behavior-rules.ts +11 -4
- package/scripts/cost-runtime/context-cut-threshold.ts +1 -9
- package/scripts/cost-runtime/cost-verdict.ts +15 -28
- package/scripts/delegation/investigation-reads.ts +5 -5
- package/scripts/discovery/repo-discovery.ts +21 -7
- package/scripts/discovery/repo-origin.ts +26 -0
- package/scripts/discovery/repo-party.ts +42 -1
- package/scripts/document/backlogrun-parent-read-set.ts +14 -15
- package/scripts/document/document-byte-budget.ts +34 -20
- package/scripts/document/entry-read-budget.ts +21 -9
- package/scripts/document/lane-child-read-set.ts +12 -14
- package/scripts/document/read-set-cli.ts +4 -4
- package/scripts/document/read-set-point-of-use.ts +10 -2
- package/scripts/document/resident-budget.ts +47 -0
- package/scripts/epic/epic-shape.ts +10 -13
- package/scripts/epic/epic-solo.ts +1 -1
- package/scripts/exports/unused-members-cli.ts +7 -3
- package/scripts/followup/git-followup-stages.ts +9 -0
- package/scripts/followup/git-pr-followup.ts +1 -1
- package/scripts/gh/git-gh-api-path.ts +7 -0
- package/scripts/gh/git-gh-issue.ts +5 -3
- package/scripts/gh/git-gh-pr-read.ts +4 -0
- package/scripts/gh/git-pr-checks-eval.ts +8 -0
- package/scripts/gh/git-pr-checks.ts +11 -1
- package/scripts/git/constants.ts +10 -0
- package/scripts/git/main-merge.ts +57 -21
- package/scripts/git/schemas.ts +2 -0
- package/scripts/hooks/pretool-guard-cli.ts +2 -2
- package/scripts/hooks/pretool-guard.ts +45 -2
- package/scripts/hooks/run-hook.sh +41 -0
- package/scripts/init/hook-command-rewrite.ts +18 -35
- package/scripts/init/hook-launch.ts +21 -32
- package/scripts/issue/issue-auto-ok.ts +95 -0
- package/scripts/issue/issue-file-cli.ts +53 -7
- package/scripts/issue/issue-file.ts +28 -5
- package/scripts/issue/issue-labels.ts +12 -12
- package/scripts/issue/issue-rank.ts +1 -1
- package/scripts/issue/issue-read-cli.ts +3 -3
- package/scripts/issue/issue-read.ts +2 -2
- package/scripts/issue/issue-scout-cli.ts +2 -2
- package/scripts/issue/issue-state-cli.ts +1 -1
- package/scripts/issue/issue-state.ts +2 -2
- package/scripts/issue/issue-wip.ts +70 -0
- package/scripts/josh/hook-decision.ts +17 -0
- package/scripts/josh/josh-commands-development.ts +11 -1
- package/scripts/josh/josh-commands-issue.ts +2 -2
- package/scripts/lane/lane-launch-cli.ts +21 -3
- package/scripts/lines/bytes-command.ts +29 -6
- package/scripts/notify/telegram-notify.ts +26 -1
- package/scripts/observations/observation-ledger-line.ts +1 -1
- package/scripts/release/release-progress.ts +167 -0
- package/scripts/release/release-publish.ts +19 -8
- package/scripts/release/release-tag.ts +22 -8
- package/scripts/rules/decision-oracle-stage.ts +1 -1
- package/scripts/rules/decision-oracle.ts +6 -6
- package/scripts/rules/delivered-rules.ts +11 -11
- package/scripts/rules/direct-pr-create.ts +110 -0
- package/scripts/rules/file-body.ts +98 -42
- package/scripts/rules/filing-cap.ts +2 -2
- package/scripts/rules/filing-offer.ts +1 -1
- package/scripts/rules/gh-api.ts +18 -1
- package/scripts/rules/implementation-cut.ts +3 -1
- package/scripts/rules/lane-background.ts +67 -10
- package/scripts/rules/permission-guards.ts +10 -2
- package/scripts/rules/pre-gate-cut.ts +2 -2
- package/scripts/rules/stop-rules.ts +4 -4
- package/scripts/rules/test-declared-commit.ts +1 -1
- package/scripts/rules/third-party-write.ts +56 -12
- package/scripts/run/run-carry.ts +11 -5
- package/scripts/run/run-cut-cli.ts +1 -1
- package/scripts/run/run-cut-handoff.ts +21 -1
- package/scripts/run/run-cut-report.ts +2 -2
- package/scripts/run/run-entry-cli.ts +11 -3
- package/scripts/run/run-hold-cli.ts +16 -2
- package/scripts/run/run-label.ts +50 -0
- package/scripts/run/run-merge-steps.ts +8 -3
- package/scripts/run/run-merge.ts +1 -1
- package/scripts/run/run-prep-cli.ts +6 -2
- package/scripts/run/run-prep.ts +17 -0
- package/scripts/run/run-ship-cli.ts +27 -6
- package/scripts/run/run-ship-next.ts +13 -0
- package/scripts/run/run-ship-pre-detach.ts +66 -0
- package/scripts/run/run-ship-preflight.ts +34 -3
- package/scripts/run/run-ship-return.ts +7 -4
- package/scripts/run/run-ship-review-steps.ts +1 -1
- package/scripts/run/run-ship-review.ts +26 -5
- package/scripts/run/run-ship-stage.ts +13 -0
- package/scripts/run/run-ship-steps.ts +10 -2
- package/scripts/run/run-ship-sync.ts +114 -0
- package/scripts/run/run-ship.ts +6 -0
- package/scripts/run/run-step-cli.ts +20 -7
- package/scripts/run/run-wake-cli.ts +8 -1
- package/scripts/run/run-wake-driver.ts +62 -8
- package/scripts/run/run-wake-session.ts +2 -1
- package/scripts/test/test-declared-logic.ts +1 -1
- package/scripts/test/test-red-logic.ts +17 -2
- package/scripts/test/test-red.ts +8 -7
- package/scripts/time-runtime/time-batch-guard.ts +52 -16
- package/scripts/time-runtime/time-density-hook.ts +20 -2
- package/scripts/time-runtime/time-density.ts +4 -3
- package/scripts/time-runtime/time-markers.ts +15 -4
- package/scripts/time-runtime/time-transcript-line.ts +27 -1
- package/scripts/version/npm-registry.ts +11 -1
- package/templates/workflows/ci.yml +9 -1
- package/.claude/skills/workflow-commands/followup-reference.md +0 -118
- package/dist/hooks/chunk-HANSKHES.js +0 -464
|
@@ -4,7 +4,12 @@ import { github_issue_url } from '#scripts/gh/github-issue-url'
|
|
|
4
4
|
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
5
5
|
import { issue_backlinks } from './issue-backlinks'
|
|
6
6
|
import { issue_classification } from './issue-classification'
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
AUTO_OK_LABEL,
|
|
9
|
+
DEPTH_LABEL_ORDER,
|
|
10
|
+
FILING_ROUTE_LABELS,
|
|
11
|
+
has_label_name,
|
|
12
|
+
} from './issue-labels'
|
|
8
13
|
import { markdown_section } from './markdown-section'
|
|
9
14
|
|
|
10
15
|
// The decisions behind `josh issue:file` (joshuafolkken/kit#2808), kept apart from the command that
|
|
@@ -31,6 +36,10 @@ interface FileArguments {
|
|
|
31
36
|
labels: ReadonlyArray<string>
|
|
32
37
|
repo: string | undefined
|
|
33
38
|
distinct: ReadonlyArray<number>
|
|
39
|
+
// `--over-cap`: the run is blocked by this filing, so the WIP cap does not hold it (`issue-wip.ts`).
|
|
40
|
+
is_over_cap: boolean
|
|
41
|
+
// `--no-auto-ok`: the Issue needs a person's judgement, so `auto-ok` is not applied (`issue-auto-ok.ts`).
|
|
42
|
+
is_auto_ok_opted_out: boolean
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
const OPTIONS = {
|
|
@@ -40,6 +49,8 @@ const OPTIONS = {
|
|
|
40
49
|
label: { type: 'string', multiple: true },
|
|
41
50
|
repo: { type: 'string' },
|
|
42
51
|
distinct: { type: 'string', multiple: true },
|
|
52
|
+
'over-cap': { type: 'boolean' },
|
|
53
|
+
'no-auto-ok': { type: 'boolean' },
|
|
43
54
|
} as const
|
|
44
55
|
|
|
45
56
|
// The label a `--depth` / `--route` value names, or `undefined` when it names none — read against the
|
|
@@ -101,7 +112,15 @@ function arguments_of(values: ParsedValues, title: string | undefined): FileArgu
|
|
|
101
112
|
|
|
102
113
|
if (labelled === undefined || !has_text(title) || !has_text(body_file)) return undefined
|
|
103
114
|
|
|
104
|
-
return {
|
|
115
|
+
return {
|
|
116
|
+
title,
|
|
117
|
+
body_file,
|
|
118
|
+
...labelled,
|
|
119
|
+
labels: values.label ?? [],
|
|
120
|
+
repo: values.repo,
|
|
121
|
+
is_over_cap: values['over-cap'] === true,
|
|
122
|
+
is_auto_ok_opted_out: values['no-auto-ok'] === true,
|
|
123
|
+
}
|
|
105
124
|
}
|
|
106
125
|
|
|
107
126
|
// An unknown flag is `undefined` rather than a throw, so the answer is the usage line, not a stack trace.
|
|
@@ -121,12 +140,16 @@ function is_undeclared_classification(label: string, body: string): boolean {
|
|
|
121
140
|
|
|
122
141
|
// Every label the filing carries: the depth, the route when there is one, the classification labels
|
|
123
142
|
// the body declares (`issue_classification.labels_for`, which merges case-insensitively), then any
|
|
124
|
-
// extra `--label
|
|
125
|
-
|
|
143
|
+
// extra `--label`, then `auto-ok` when the filing opted in (`issue-auto-ok.ts`) and no `--label`
|
|
144
|
+
// already named it. One request applies them all, so no label is a separate step to forget.
|
|
145
|
+
function labels_of(args: FileArguments, body: string, is_auto_ok = false): ReadonlyArray<string> {
|
|
126
146
|
const extra = args.labels.filter((label) => !is_undeclared_classification(label, body))
|
|
127
147
|
const declared = [args.depth, ...(args.route === undefined ? [] : [args.route]), ...extra]
|
|
148
|
+
const labels = issue_classification.labels_for(body, declared)
|
|
128
149
|
|
|
129
|
-
|
|
150
|
+
if (!is_auto_ok || has_label_name(labels, AUTO_OK_LABEL)) return labels
|
|
151
|
+
|
|
152
|
+
return [...labels, AUTO_OK_LABEL]
|
|
130
153
|
}
|
|
131
154
|
|
|
132
155
|
function is_same_repository(target: string, current: string): boolean {
|
|
@@ -16,7 +16,7 @@ const PLANNED_LABEL = 'run:planned'
|
|
|
16
16
|
// forever (joshuafolkken/kit#860).
|
|
17
17
|
const NEEDS_DECISION_LABEL = 'needs-decision'
|
|
18
18
|
// Marks an issue whose defect reaches the verification path, so it runs with nothing beside it
|
|
19
|
-
// (joshuafolkken/kit#2776). A run applies it from the verification-path enumeration in `
|
|
19
|
+
// (joshuafolkken/kit#2776). A run applies it from the verification-path enumeration in `backlogrun-lanes.md`;
|
|
20
20
|
// `backlog:next` enforces it (`backlog-solo.ts`), so the rule no longer rests on a judgement at
|
|
21
21
|
// dispatch time.
|
|
22
22
|
const RUN_SOLO_LABEL = 'run:solo'
|
|
@@ -36,11 +36,12 @@ const PRIORITY_HIGH_LABEL = 'priority:high'
|
|
|
36
36
|
const AUTO_OK_LABEL = 'auto-ok'
|
|
37
37
|
// Degrades one issue's run to a `halfrun`-shaped stop: it is implemented and taken through the
|
|
38
38
|
// verification gate, and then nothing is committed, pushed, opened as a pull request or merged
|
|
39
|
-
// (joshuafolkken/kit#1125). **Only a person applies it
|
|
40
|
-
//
|
|
39
|
+
// (joshuafolkken/kit#1125). **Only a person applies it** — a mark a run could clear for itself is not
|
|
40
|
+
// a mark.
|
|
41
41
|
//
|
|
42
|
-
// It is the opposite of `auto-ok` in what it does
|
|
43
|
-
//
|
|
42
|
+
// It is the opposite of `auto-ok` in what it does: one widens unattended execution past an epic's
|
|
43
|
+
// edge, the other withholds the last step of it. Unlike `auto-ok`, which `issue:file` applies by
|
|
44
|
+
// default to work found by opted-in work (joshuafolkken/kit#3213), no run ever applies it. `auto-ok`
|
|
44
45
|
// answers "may this run at all", this one answers "may its result ship without a person looking".
|
|
45
46
|
//
|
|
46
47
|
// **Not `needs-decision`, and the difference is what the two sets below encode.** A parked issue is
|
|
@@ -154,7 +155,7 @@ const FILING_ROUTE_LABELS: ReadonlyArray<{
|
|
|
154
155
|
//
|
|
155
156
|
// **The depth is read off the subject, so a run applies it.** Unlike `auto-ok` and
|
|
156
157
|
// `needs-human-review` this is not a person's judgement about authorization; it is the same reading
|
|
157
|
-
//
|
|
158
|
+
// `observation-filing.md` already asks a run to make before it files, and none of the three withholds or widens anything
|
|
158
159
|
// a run may do.
|
|
159
160
|
const DEPTH_0_LABEL = 'depth:0'
|
|
160
161
|
const DEPTH_1_LABEL = 'depth:1'
|
|
@@ -171,9 +172,8 @@ const DEPTH_LABEL_ORDER: ReadonlyArray<string> = [DEPTH_0_LABEL, DEPTH_1_LABEL,
|
|
|
171
172
|
// description, so provisioning from here is what gives each one a stable color a reader can scan by.
|
|
172
173
|
// Green, amber and pale blue, so the consumer-facing depth is the one that stands out in a listing.
|
|
173
174
|
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
// those three command lines to this array, so a color changed here without the document fails.
|
|
175
|
+
// **`josh issue:file` provisions them** through `repository_labels.ensure_labels` before its create
|
|
176
|
+
// call (joshuafolkken/kit#3176), so no document carries a creation command to keep in step.
|
|
177
177
|
const DEPTH_LABELS: ReadonlyArray<{
|
|
178
178
|
name: string
|
|
179
179
|
color: string
|
|
@@ -182,17 +182,17 @@ const DEPTH_LABELS: ReadonlyArray<{
|
|
|
182
182
|
{
|
|
183
183
|
name: DEPTH_0_LABEL,
|
|
184
184
|
color: '0e8a16',
|
|
185
|
-
description: 'Depth 0 — what a consumer of this package touches (
|
|
185
|
+
description: 'Depth 0 — what a consumer of this package touches (observation-filing.md)',
|
|
186
186
|
},
|
|
187
187
|
{
|
|
188
188
|
name: DEPTH_1_LABEL,
|
|
189
189
|
color: 'fbc02d',
|
|
190
|
-
description: 'Depth 1 — the run orchestration that executes an Issue (
|
|
190
|
+
description: 'Depth 1 — the run orchestration that executes an Issue (observation-filing.md)',
|
|
191
191
|
},
|
|
192
192
|
{
|
|
193
193
|
name: DEPTH_2_LABEL,
|
|
194
194
|
color: 'c5def5',
|
|
195
|
-
description: 'Depth 2 — what measures a run (
|
|
195
|
+
description: 'Depth 2 — what measures a run (observation-filing.md)',
|
|
196
196
|
},
|
|
197
197
|
]
|
|
198
198
|
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
//
|
|
16
16
|
// **`run:solo` alone is not a key.** It means "runs alone", not "runs first": tidying that only touches
|
|
17
17
|
// the verification path carries it too, and a low-priority one is not owed a place ahead of the queue.
|
|
18
|
-
// Only a defect on that path is, because `
|
|
18
|
+
// Only a defect on that path is, because `backlogrun-lanes.md` holds a batch until it has merged.
|
|
19
19
|
|
|
20
20
|
interface RankRow {
|
|
21
21
|
labels: ReadonlyArray<string>
|
|
@@ -8,7 +8,7 @@ import { issue_report_failures, type ReadFailureKind } from './issue-report-fail
|
|
|
8
8
|
// `josh issue:read <N> [<N> ...]` — the body *and* the comments of every issue named, in one call
|
|
9
9
|
// (joshuafolkken/kit#1715).
|
|
10
10
|
//
|
|
11
|
-
// It replaces the two `gh api` reads `.claude/skills/workflow-commands/
|
|
11
|
+
// It replaces the two `gh api` reads `.claude/skills/workflow-commands/issue-comments.md` tells an agent
|
|
12
12
|
// to type per issue. Measured over four recorded `backlogrun` parents, `issue bookkeeping` was the
|
|
13
13
|
// largest single contributor to the parent's turn count — 110 of 414 turns, 26.6% — and one issue
|
|
14
14
|
// read at a time was its dominant shape: 19 `…/issues/<N>` calls and 5 `…/comments` calls, each its
|
|
@@ -23,7 +23,7 @@ import { issue_report_failures, type ReadFailureKind } from './issue-report-fail
|
|
|
23
23
|
// prevent.
|
|
24
24
|
//
|
|
25
25
|
// **A failed read is never printed as an issue**, and neither is a failed comment listing printed as
|
|
26
|
-
// "no comments" — the block says which of the two it holds, because
|
|
26
|
+
// "no comments" — the block says which of the two it holds, because `issue-comments.md`'s rule is that the later text
|
|
27
27
|
// wins, and a comment nobody read cannot win anything.
|
|
28
28
|
|
|
29
29
|
const FAILURE_EXIT_CODE = 1
|
|
@@ -88,7 +88,7 @@ async function read_issue(issue_number: string): Promise<NumberResult> {
|
|
|
88
88
|
|
|
89
89
|
// One issue's block for a caller that wants the text rather than the printing — `run:prep` composes it
|
|
90
90
|
// with the state block and the dependency-update line. A failed read is returned as its kind, never as
|
|
91
|
-
// an empty block, so
|
|
91
|
+
// an empty block, so `issue-comments.md`'s "a comment nobody read cannot win anything" holds through the bundle too.
|
|
92
92
|
async function read_block(issue_number: string): Promise<BlockRead> {
|
|
93
93
|
const result = await read_issue(issue_number)
|
|
94
94
|
|
|
@@ -6,7 +6,7 @@ import { z } from 'zod'
|
|
|
6
6
|
// (joshuafolkken/kit#1715).
|
|
7
7
|
//
|
|
8
8
|
// **It exists because reading one issue costs a parent two turns.** `.claude/skills/workflow-commands/SKILL.md`
|
|
9
|
-
//
|
|
9
|
+
// `issue-comments.md` requires the body *and* the comments, since a decision recorded after the body was written
|
|
10
10
|
// lives only in a comment; typed by hand that is `gh api repos/{owner}/{repo}/issues/<N>` followed by
|
|
11
11
|
// `gh api repos/{owner}/{repo}/issues/<N>/comments`. Measured over four recorded `backlogrun`
|
|
12
12
|
// parents, `issue bookkeeping` was the single largest contributor to the parent's turn count — 110 of
|
|
@@ -15,7 +15,7 @@ import { z } from 'zod'
|
|
|
15
15
|
// distribution this collapses.
|
|
16
16
|
//
|
|
17
17
|
// **The comments are never silently absent.** A listing that could not be read prints a line saying
|
|
18
|
-
// so, because a block showing no comment where the read failed is exactly the misread
|
|
18
|
+
// so, because a block showing no comment where the read failed is exactly the misread `issue-comments.md` exists to
|
|
19
19
|
// prevent: the agreement in force would be taken from a body a comment had already overturned.
|
|
20
20
|
|
|
21
21
|
const ISSUE_LABEL = 'issue: '
|
|
@@ -107,7 +107,7 @@ function format_duplicate(candidate: DuplicateCandidate): string {
|
|
|
107
107
|
const epic = candidate.epic === undefined ? '' : ` (epic #${String(candidate.epic)})`
|
|
108
108
|
// The reader does two different things with the two states, so the row has to say which it is: an
|
|
109
109
|
// open candidate means somebody is already tracking this, a closed one means it may already be
|
|
110
|
-
// done — and the second is the exit `
|
|
110
|
+
// done — and the second is the exit `issue-comments.md` names, not a second filing.
|
|
111
111
|
const closed = candidate.is_closed === true ? ' (closed)' : ''
|
|
112
112
|
|
|
113
113
|
return ` #${String(candidate.number)} ${score} ${candidate.title}${closed}${epic}`
|
|
@@ -211,7 +211,7 @@ function named_epics(draft: BacklogIssue, issues: ReadonlyArray<BacklogIssue>):
|
|
|
211
211
|
.map((issue) => issue.number)
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
// A person naming the epic is the strongest signal there is — it is what
|
|
214
|
+
// A person naming the epic is the strongest signal there is — it is what `into-target.md`'s `into <target>` suffix
|
|
215
215
|
// means — so it is reported ahead of whatever the candidate search concluded, with the candidates
|
|
216
216
|
// still named beside it.
|
|
217
217
|
function format_named_epics(epics: ReadonlyArray<number>, decision: BundleDecision): Array<string> {
|
|
@@ -170,7 +170,7 @@ async function read_all(request: StateRequest): Promise<ReadonlyArray<IssueRepor
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
// `[]` for a number that produced no state, so one unresolvable number costs the others nothing.
|
|
173
|
-
// The heading is withheld for a single number: `.claude/skills/workflow-commands/
|
|
173
|
+
// The heading is withheld for a single number: `.claude/skills/workflow-commands/needs-human-review.md` and
|
|
174
174
|
// `.claude/skills/diag/SKILL.md` read that report's three lines verbatim.
|
|
175
175
|
function state_blocks(report: IssueReport, should_attribute: boolean): ReadonlyArray<string> {
|
|
176
176
|
if (report.result.kind !== 'state') return []
|
|
@@ -88,8 +88,8 @@ function format_issue_state(state: IssueState): string {
|
|
|
88
88
|
|
|
89
89
|
// The same three lines, under the number they belong to. Kept as a separate function rather than a
|
|
90
90
|
// flag on the one above, so the single-number report has no branch that could ever prepend a fourth
|
|
91
|
-
// line to it: `.claude/skills/workflow-commands/
|
|
92
|
-
// read those three verbatim, and
|
|
91
|
+
// line to it: `.claude/skills/workflow-commands/needs-human-review.md` and `.claude/skills/diag/SKILL.md`
|
|
92
|
+
// read those three verbatim, and `needs-human-review.md`'s `needs-human-review` stop is decided from them.
|
|
93
93
|
function format_attributed_issue_state(issue_number: string, state: IssueState): string {
|
|
94
94
|
return `${ISSUE_LABEL}${issue_number}\n${format_issue_state(state)}`
|
|
95
95
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { delivered_rules } from '#scripts/rules/delivered-rules'
|
|
2
|
+
import { INTERRUPT_ROUTE_LABEL, SPLIT_ROUTE_LABEL, TIER_A_ROUTE_LABEL } from './issue-labels'
|
|
3
|
+
|
|
4
|
+
// The WIP cap at the call that files (joshuafolkken/kit#3181). The count used to be a hand-run
|
|
5
|
+
// `gh api … | wc -l` the agent compared with the cap itself; `josh issue:file` now reads it and asks
|
|
6
|
+
// the exemption question before anything is sent, the same hold `--distinct` places on a duplicate.
|
|
7
|
+
// What stays judgement — whether a filing is exempt — is declared by the route or by `--over-cap`.
|
|
8
|
+
|
|
9
|
+
const WITHIN = 'within'
|
|
10
|
+
const EXEMPT = 'exempt'
|
|
11
|
+
const HELD = 'held' as const
|
|
12
|
+
|
|
13
|
+
type WipVerdict = typeof WITHIN | typeof EXEMPT | typeof HELD
|
|
14
|
+
|
|
15
|
+
// Each route is an exempt category by definition: a prerequisite or upstream defect (`tier-a`) and a
|
|
16
|
+
// split child block the run, and an interrupt is one of the three tests. `review-cap` is the review
|
|
17
|
+
// round cap's branch 2, which `wip-cap.md` counts as discretionary, so it is held like no route at all.
|
|
18
|
+
const EXEMPT_ROUTES: ReadonlySet<string> = new Set([
|
|
19
|
+
TIER_A_ROUTE_LABEL,
|
|
20
|
+
SPLIT_ROUTE_LABEL,
|
|
21
|
+
INTERRUPT_ROUTE_LABEL,
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
const HELD_MESSAGE =
|
|
25
|
+
'✖ over the WIP cap: close one first; nothing honestly closable means do not file — a discretionary ' +
|
|
26
|
+
'filing waits. An interrupt meeting one of three tests (a verification answers wrongly, a documented ' +
|
|
27
|
+
'workflow cannot complete, or data is lost or written outside the repository) files with ' +
|
|
28
|
+
'`--route interrupt`; a filing the run is blocked by reissues with `--over-cap`. Either way, state ' +
|
|
29
|
+
'the overage (`prompts/collaboration-workflow/wip-cap.md`).'
|
|
30
|
+
|
|
31
|
+
const UNREAD_MESSAGE =
|
|
32
|
+
'⚠ wip: could not count the open Issues — the filing goes on unchecked against the cap ' +
|
|
33
|
+
'(`prompts/collaboration-workflow/wip-cap.md`).'
|
|
34
|
+
|
|
35
|
+
interface WipFiling {
|
|
36
|
+
route: string | undefined
|
|
37
|
+
is_over_cap: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function is_exempt(filing: WipFiling): boolean {
|
|
41
|
+
return filing.is_over_cap || (filing.route !== undefined && EXEMPT_ROUTES.has(filing.route))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The cap is "more than `WIP_CAP` open", so exactly the cap is still within it.
|
|
45
|
+
function verdict_of(count: number, filing: WipFiling): WipVerdict {
|
|
46
|
+
if (count <= delivered_rules.WIP_CAP) return WITHIN
|
|
47
|
+
|
|
48
|
+
return is_exempt(filing) ? EXEMPT : HELD
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function count_line(count: number, repo: string, verdict: WipVerdict): string {
|
|
52
|
+
return `wip: ${String(count)} open in ${repo} · cap ${String(delivered_rules.WIP_CAP)} · ${verdict}`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The listing's JSON is an array of rows; anything else is unreadable rather than zero, because zero
|
|
56
|
+
// would pass every filing as within the cap.
|
|
57
|
+
function count_of(json: string): number | undefined {
|
|
58
|
+
try {
|
|
59
|
+
const parsed: unknown = JSON.parse(json)
|
|
60
|
+
|
|
61
|
+
return Array.isArray(parsed) ? parsed.length : undefined
|
|
62
|
+
} catch {
|
|
63
|
+
return undefined
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const issue_wip = { HELD, HELD_MESSAGE, UNREAD_MESSAGE, verdict_of, count_line, count_of }
|
|
68
|
+
|
|
69
|
+
export type { WipVerdict }
|
|
70
|
+
export { issue_wip }
|
|
@@ -38,6 +38,7 @@ type HookPayload = z.infer<typeof payload_schema>
|
|
|
38
38
|
|
|
39
39
|
const HOOK_EVENT_NAME = 'PreToolUse'
|
|
40
40
|
const DENY_DECISION = 'deny'
|
|
41
|
+
const ALLOW_DECISION = 'allow'
|
|
41
42
|
// No record means nothing has been refused yet, and every span began after instant zero — which is
|
|
42
43
|
// the answer wanted: the first qualifying accumulation of a run is refused.
|
|
43
44
|
const NEVER_MS = 0
|
|
@@ -84,6 +85,21 @@ function deny_envelope(reason: string): string {
|
|
|
84
85
|
})
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
// **A rewrite, not a refusal** (joshuafolkken/kit#3154): the call proceeds with `updated_input` merged
|
|
89
|
+
// over its own. `allow` is the decision the harness applies an `updatedInput` under; a settings `deny`
|
|
90
|
+
// rule still wins over it, so this never widens what the call may do.
|
|
91
|
+
function rewrite_envelope(updated_input: Record<string, unknown>, context: string): string {
|
|
92
|
+
return JSON.stringify({
|
|
93
|
+
systemMessage: context,
|
|
94
|
+
hookSpecificOutput: {
|
|
95
|
+
hookEventName: HOOK_EVENT_NAME,
|
|
96
|
+
permissionDecision: ALLOW_DECISION,
|
|
97
|
+
updatedInput: updated_input,
|
|
98
|
+
additionalContext: context,
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
87
103
|
// **A notice, not a decision.** It carries no `permissionDecision`, so the call proceeds exactly as it
|
|
88
104
|
// would have; `additionalContext` is what reaches the model, and `systemMessage` is what the person
|
|
89
105
|
// watching sees. Both are filled because either channel alone leaves one of the two blind — and the
|
|
@@ -470,6 +486,7 @@ const hook_decision = {
|
|
|
470
486
|
notice_envelope,
|
|
471
487
|
parse_hook_payload,
|
|
472
488
|
report_no_payload,
|
|
489
|
+
rewrite_envelope,
|
|
473
490
|
stamp_fault,
|
|
474
491
|
write_decision,
|
|
475
492
|
write_outcome,
|
|
@@ -115,6 +115,16 @@ const DEV_COMMANDS: Record<string, CommandEntry> = {
|
|
|
115
115
|
// `UserPromptSubmit`, so it must stay eligible for in-process dispatch rather than pay a second
|
|
116
116
|
// tsx start each turn. It calls `process.loadEnvFile` itself through the shared loader.
|
|
117
117
|
},
|
|
118
|
+
'codex:hook-adapter': {
|
|
119
|
+
script: 'scripts/hooks/codex-hook-adapter.ts',
|
|
120
|
+
description:
|
|
121
|
+
'Codex hook: run the pretool or posttool guard on a Codex payload (reads the tool call on stdin)',
|
|
122
|
+
category: 'Development',
|
|
123
|
+
reference: ['<pretool|posttool>', 'automation', ['none']],
|
|
124
|
+
// The live-source fallback `scripts/hooks/run-hook.sh` derives from the `codex-hook-adapter`
|
|
125
|
+
// bundle name (joshuafolkken/kit#3184). **No `tsx_arguments`**, the same as `pretool:guard`
|
|
126
|
+
// above: it runs before every guarded Codex call.
|
|
127
|
+
},
|
|
118
128
|
'cspell:dot': {
|
|
119
129
|
script: 'scripts/lint/cspell-cached.ts',
|
|
120
130
|
description: 'Run spell check including dotfiles',
|
|
@@ -158,7 +168,7 @@ const DEV_COMMANDS: Record<string, CommandEntry> = {
|
|
|
158
168
|
'test:red': {
|
|
159
169
|
script: 'scripts/test/test-red.ts',
|
|
160
170
|
description:
|
|
161
|
-
'Run the changed unit tests against the pre-fix tree (merge-base worktree) and report red/green/no-test',
|
|
171
|
+
'Run the changed unit tests against the pre-fix tree (merge-base worktree) and report red/green/no-test/test-only',
|
|
162
172
|
category: 'Development',
|
|
163
173
|
reference: ['', 'developer', ['git', 'processes']],
|
|
164
174
|
},
|
|
@@ -66,10 +66,10 @@ const ISSUE_COMMANDS: Record<string, CommandEntry> = {
|
|
|
66
66
|
'issue:file': {
|
|
67
67
|
script: 'scripts/issue/issue-file-cli.ts',
|
|
68
68
|
description:
|
|
69
|
-
'File an Issue with every filing step: lint, Origin, duplicate scout, labels, then epic:bundle',
|
|
69
|
+
'File an Issue with every filing step: lint, Origin, WIP cap, duplicate scout, labels, then epic:bundle',
|
|
70
70
|
category: 'AI tools',
|
|
71
71
|
reference: [
|
|
72
|
-
'<title> --body-file <path> --depth <0|1|2> [--route <route>] [--label <name>] [--repo <owner/repo>] [--distinct <N,…>]',
|
|
72
|
+
'<title> --body-file <path> --depth <0|1|2> [--route <route>] [--label <name>] [--repo <owner/repo>] [--distinct <N,…>] [--over-cap] [--no-auto-ok]',
|
|
73
73
|
'automation',
|
|
74
74
|
['network'],
|
|
75
75
|
],
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { josh_command } from '#scripts/josh/josh-run'
|
|
4
|
+
import { lane_registry } from '#scripts/lane/lane-registry'
|
|
4
5
|
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
5
6
|
import { INSTALL_TIMEOUT_MS } from '#scripts/lib/timeouts'
|
|
6
7
|
import { execa } from 'execa'
|
|
@@ -15,6 +16,7 @@ import { execa } from 'execa'
|
|
|
15
16
|
// and messages are reused rather than cloned — `lane:open`'s cut-session guard and its `full` /
|
|
16
17
|
// `already-open` refusals, `stash:pop`'s message-matched pop, `lane:dispatch`'s in-progress claim and
|
|
17
18
|
// warning. Their explanations stream through to stderr; the child's pid is the one thing on stdout.
|
|
19
|
+
// A lane a park kept is the one `already-open` it steps past — `lane_directory` below.
|
|
18
20
|
|
|
19
21
|
const ARGV_OFFSET = 2
|
|
20
22
|
const SUCCESS_EXIT_CODE = 0
|
|
@@ -86,14 +88,30 @@ type LaunchOutcome = { kind: 'launched'; pid: string } | { kind: 'unopened' } |
|
|
|
86
88
|
const UNOPENED: LaunchOutcome = { kind: 'unopened' }
|
|
87
89
|
const FAILED: LaunchOutcome = { kind: 'failed' }
|
|
88
90
|
|
|
91
|
+
// **A lane a park kept open is resumed, not reopened** (joshuafolkken/kit#3228). A child parked before
|
|
92
|
+
// its commit leaves its uncommitted work in its lane, and `lane:open` refuses that lane as
|
|
93
|
+
// `already-open` — so the released child is dispatched into the kept tree instead. **Only a lane a
|
|
94
|
+
// child was already dispatched into counts as kept**: `lane:dispatch` records the child's output in the
|
|
95
|
+
// lane's `.env`, and a lane whose install failed never got that far, so it still goes through
|
|
96
|
+
// `lane:open` and its refusal. So does a stranded lane, whose `.env` is gone with its tree.
|
|
97
|
+
async function lane_directory(issue: string): Promise<string | undefined> {
|
|
98
|
+
const kept = await lane_registry.find_open_lane(issue)
|
|
99
|
+
|
|
100
|
+
if (kept?.output !== undefined) return kept.directory
|
|
101
|
+
|
|
102
|
+
const opened = await josh_command.josh_run(['lane:open', issue], should_forward_stderr)
|
|
103
|
+
|
|
104
|
+
return opened.code === SUCCESS_EXIT_CODE ? opened.out : undefined
|
|
105
|
+
}
|
|
106
|
+
|
|
89
107
|
// The launch chain in-process. The CLI prints the pid; `backlog:drive` launches through this same
|
|
90
108
|
// chain (joshuafolkken/kit#2508).
|
|
91
109
|
async function launch_lane(context: LaunchContext): Promise<LaunchOutcome> {
|
|
92
|
-
const
|
|
110
|
+
const directory = await lane_directory(context.issue)
|
|
93
111
|
|
|
94
|
-
if (
|
|
112
|
+
if (directory === undefined) return UNOPENED
|
|
95
113
|
|
|
96
|
-
if (!(await prepare(context.stash,
|
|
114
|
+
if (!(await prepare(context.stash, directory))) return FAILED
|
|
97
115
|
|
|
98
116
|
const dispatched = await josh_command.josh_run(
|
|
99
117
|
['lane:dispatch', context.issue],
|
|
@@ -4,6 +4,7 @@ import path from 'node:path'
|
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
5
|
import { document_byte_budget } from '#scripts/document/document-byte-budget'
|
|
6
6
|
import { entry_read_budget } from '#scripts/document/entry-read-budget'
|
|
7
|
+
import { resident_budget } from '#scripts/document/resident-budget'
|
|
7
8
|
|
|
8
9
|
// The package root, resolved from this file rather than the caller's working directory.
|
|
9
10
|
const REPO_ROOT = fileURLToPath(new URL('../../', import.meta.url))
|
|
@@ -75,14 +76,40 @@ function normalized_path(argument: string): string {
|
|
|
75
76
|
return argument.startsWith('./') ? argument.slice('./'.length) : argument
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
function remaining_phrase(remaining: number): string {
|
|
80
|
+
return remaining < 0 ? `over by ${(-remaining).toString()}` : `${remaining.toString()} left`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The resident document is held by a fixed limit rather than a block ratchet, and loosening it is
|
|
84
|
+
// Tier C, so its row names the headroom or the overage but never a value to raise it to
|
|
85
|
+
// (joshuafolkken/kit#3171).
|
|
86
|
+
function resident_row(status: DocumentStatus): string {
|
|
87
|
+
const counts = `${status.current.toString()}/${status.recorded.toString()} bytes`
|
|
88
|
+
|
|
89
|
+
return `${status.path}${ROW_GAP}${counts} · ${remaining_phrase(status.remaining)}`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function is_resident(relative_path: string): boolean {
|
|
93
|
+
return resident_budget.limit_bytes_for(relative_path) !== undefined
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function recorded_for(relative_path: string): number | undefined {
|
|
97
|
+
return (
|
|
98
|
+
resident_budget.limit_bytes_for(relative_path) ??
|
|
99
|
+
document_byte_budget.recorded_bytes_for(relative_path)
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
78
103
|
function argument_row(argument: string, root: string = REPO_ROOT): string {
|
|
79
104
|
const relative_path = normalized_path(argument)
|
|
80
|
-
const recorded =
|
|
105
|
+
const recorded = recorded_for(relative_path)
|
|
81
106
|
|
|
82
107
|
if (recorded === undefined) return `${relative_path}${ROW_GAP}${NOT_BUDGETED}`
|
|
83
108
|
if (!existsSync(path.join(root, relative_path))) return `${relative_path}${ROW_GAP}${NOT_A_FILE}`
|
|
84
109
|
|
|
85
|
-
|
|
110
|
+
const status = status_of(relative_path, recorded, root)
|
|
111
|
+
|
|
112
|
+
return is_resident(relative_path) ? resident_row(status) : status_row(status)
|
|
86
113
|
}
|
|
87
114
|
|
|
88
115
|
// A document is "near its ceiling" once less than this is left before its recorded block ceiling —
|
|
@@ -118,10 +145,6 @@ function entry_status_of(entry: string, recorded: number): EntryStatus {
|
|
|
118
145
|
return { entry, current, recorded, remaining }
|
|
119
146
|
}
|
|
120
147
|
|
|
121
|
-
function remaining_phrase(remaining: number): string {
|
|
122
|
-
return remaining < 0 ? `over by ${(-remaining).toString()}` : `${remaining.toString()} left`
|
|
123
|
-
}
|
|
124
|
-
|
|
125
148
|
// The primary budget, one row per entry: the total each workflow entry reads against its recorded
|
|
126
149
|
// ceiling and the headroom left — printed on every scan, so the main budget is no longer a number the
|
|
127
150
|
// gate reveals only when it fails (joshuafolkken/kit#2271).
|
|
@@ -3,6 +3,10 @@ import { timed_fetch } from '#scripts/lib/timed-fetch'
|
|
|
3
3
|
import { z } from 'zod'
|
|
4
4
|
|
|
5
5
|
const TELEGRAM_API_BASE = 'https://api.telegram.org'
|
|
6
|
+
// `sendMessage` refuses a text longer than this.
|
|
7
|
+
const TELEGRAM_TEXT_LIMIT = 4096
|
|
8
|
+
const BODY_TRUNCATION_MARK = '\n...'
|
|
9
|
+
const TRAILING_HIGH_SURROGATE = /[\uD800-\uDBFF]$/u
|
|
6
10
|
|
|
7
11
|
const telegram_environment_schema = z.object({
|
|
8
12
|
telegram_bot_token: z.string().min(1, { message: 'TELEGRAM_BOT_TOKEN is required' }),
|
|
@@ -188,10 +192,31 @@ function build_blocks(input: TelegramSendInput): Array<string> {
|
|
|
188
192
|
return blocks
|
|
189
193
|
}
|
|
190
194
|
|
|
191
|
-
function
|
|
195
|
+
function join_blocks(input: TelegramSendInput): string {
|
|
192
196
|
return build_blocks(input).join('\n\n')
|
|
193
197
|
}
|
|
194
198
|
|
|
199
|
+
// A cut between the two halves of a surrogate pair leaves a lone surrogate, which is not valid
|
|
200
|
+
// UTF-8 once encoded, so the half is dropped with the rest of the tail.
|
|
201
|
+
function head_of(body: string, kept: number): string {
|
|
202
|
+
const head = body.slice(0, kept)
|
|
203
|
+
|
|
204
|
+
return TRAILING_HIGH_SURROGATE.test(head) ? head.slice(0, -1) : head
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// **The body is cut to fit Telegram's length limit, keeping its head** (joshuafolkken/kit#3242). A
|
|
208
|
+
// warning carrying a whole driver stderr ran past 4096 characters and was refused with `400 Bad
|
|
209
|
+
// Request`, so it reached nobody. A caller puts the cause first, so the tail is what is dropped, and the
|
|
210
|
+
// header and the URLs around the body survive.
|
|
211
|
+
function build_text(input: TelegramSendInput): string {
|
|
212
|
+
const text = join_blocks(input)
|
|
213
|
+
const overflow = text.length - TELEGRAM_TEXT_LIMIT
|
|
214
|
+
if (overflow <= 0 || input.body === undefined) return text
|
|
215
|
+
const kept = Math.max(0, input.body.length - overflow - BODY_TRUNCATION_MARK.length)
|
|
216
|
+
|
|
217
|
+
return join_blocks({ ...input, body: `${head_of(input.body, kept)}${BODY_TRUNCATION_MARK}` })
|
|
218
|
+
}
|
|
219
|
+
|
|
195
220
|
async function post_message(config: TelegramConfig, text: string): Promise<void> {
|
|
196
221
|
const url = `${TELEGRAM_API_BASE}/bot${config.bot_token}/sendMessage`
|
|
197
222
|
const response = await timed_fetch(url, {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The observation ledger's line grammar (`.claude/skills/workflow-commands/observation-
|
|
1
|
+
// The observation ledger's line grammar (`.claude/skills/workflow-commands/observation-ledger.md`)
|
|
2
2
|
// is fixed — `- k:<slug> | d<n> | <YYYY-MM-DD> | <where> | <what>` — but the path was collected in
|
|
3
3
|
// `observation-ledger.ts` without a line ever being parsed, so a malformed append rode a flush
|
|
4
4
|
// through unremarked. This validates a ledger entry line and reports why a broken one is broken
|