@joshuafolkken/kit 1.1453.0 → 1.1471.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/skills/dependency-update/SKILL.md +26 -55
- package/.claude/skills/epic-commands/SKILL.md +87 -148
- package/.claude/skills/epic-commands/epic-bundle.md +45 -77
- package/.claude/skills/epic-commands/execution-waves.md +44 -81
- package/.claude/skills/verify-ui/SKILL.md +25 -50
- package/.claude/skills/workflow-commands/background-commands.md +36 -47
- package/.claude/skills/workflow-commands/backlogrun-child.md +72 -85
- package/.claude/skills/workflow-commands/backlogrun-handoff-report.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +91 -121
- package/.claude/skills/workflow-commands/backlogrun-park.md +73 -117
- package/.claude/skills/workflow-commands/backlogrun-progress.md +7 -9
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +11 -20
- package/.claude/skills/workflow-commands/backlogrun-steps.md +43 -52
- package/.claude/skills/workflow-commands/chain-rule.md +24 -34
- package/.claude/skills/workflow-commands/followup.md +57 -76
- package/.claude/skills/workflow-commands/halfrun.md +2 -1
- package/.claude/skills/workflow-commands/issue-comments.md +43 -60
- package/.claude/skills/workflow-commands/kickoff.md +65 -74
- package/.claude/skills/workflow-commands/observation-filing.md +62 -107
- package/.claude/skills/workflow-commands/observation-ledger.md +56 -111
- package/.claude/skills/workflow-commands/pre-gate-cut.md +89 -138
- package/.claude/skills/workflow-commands/prerequisite.md +10 -15
- package/.claude/skills/workflow-commands/progress-watcher.md +21 -26
- package/.claude/skills/workflow-commands/prrun.md +2 -1
- package/.claude/skills/workflow-commands/retrospective.md +31 -50
- package/.claude/skills/workflow-commands/split-assessment.md +34 -58
- package/.claude/skills/workflow-commands/working-tree-hold.md +54 -71
- package/CLAUDE.md +7 -6
- package/README.md +2 -9
- package/dist/CLAUDE.md +7 -6
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/{chunk-Y3GKIKGW.js → chunk-43ZHT22P.js} +31 -15
- package/dist/hooks/{chunk-MBJYQPEK.js → chunk-AS74OU3D.js} +24 -8
- package/dist/hooks/{chunk-3C4XRYG6.js → chunk-E7OIKX5Z.js} +17 -12
- package/dist/hooks/{chunk-Z4XI7CZS.js → chunk-EUMACUK2.js} +18 -12
- package/dist/hooks/{chunk-NO4AALP5.js → chunk-H5HVWYXK.js} +4 -4
- package/dist/hooks/{chunk-5C7L6FRC.js → chunk-IIGFYXKC.js} +346 -241
- package/dist/hooks/{chunk-W5ZKGTSY.js → chunk-J47HU5H5.js} +14 -3
- package/dist/hooks/{chunk-OLMO72ON.js → chunk-K4EOONYM.js} +1 -1
- package/dist/hooks/{chunk-LBFYEOXJ.js → chunk-K4LMBQC2.js} +4 -4
- package/dist/hooks/{chunk-D3NJXN5M.js → chunk-KC5AK2GF.js} +8 -6
- package/dist/hooks/chunk-SHGPANFB.js +56 -0
- package/dist/hooks/{chunk-EF5GQ6WS.js → chunk-YHBZAJSV.js} +3 -3
- package/dist/hooks/{chunk-SHYUQOIS.js → chunk-YYPI7SJZ.js} +5 -5
- package/dist/hooks/codex-hook-adapter.js +13 -13
- package/dist/hooks/duplicate-read-guard.js +7 -7
- package/dist/hooks/format-edited.js +3 -3
- package/dist/hooks/inputs.json +251 -247
- package/dist/hooks/investigation-guard.js +7 -7
- package/dist/hooks/pretool-guard.js +12 -12
- package/dist/hooks/session-lang.js +6 -5
- package/dist/hooks/stop-guard.js +75 -18
- package/dist/josh.js +6 -7
- package/dist/managed-marker/index.js +1 -2
- package/dist/version/index.js +15 -12
- package/eslint/base.js +2 -1
- package/eslint/rules/unicorn.js +16 -0
- package/package.json +3 -3
- package/prompts/coding-standards.md +12 -22
- package/prompts/collaboration-workflow/file-edits.md +26 -44
- package/prompts/collaboration-workflow/gh-rest.md +20 -36
- package/prompts/collaboration-workflow/glossary.md +5 -7
- package/prompts/collaboration-workflow/issue-citation.md +7 -13
- package/prompts/collaboration-workflow/issue-template.md +29 -29
- package/prompts/collaboration-workflow/operating-rules.md +35 -44
- package/prompts/collaboration-workflow/output-bounds.md +15 -15
- package/prompts/collaboration-workflow/overview.md +14 -19
- package/prompts/collaboration-workflow/plan-comment.md +13 -13
- package/prompts/collaboration-workflow/principles.md +50 -53
- package/prompts/collaboration-workflow/proposal-request.md +1 -9
- package/prompts/collaboration-workflow/report-format.md +32 -49
- package/prompts/collaboration-workflow/residency.md +10 -10
- package/prompts/collaboration-workflow/rule-delivery.md +9 -9
- package/prompts/collaboration-workflow/shell-body.md +11 -21
- package/prompts/collaboration-workflow/turn-batching.md +8 -14
- package/prompts/collaboration-workflow/upstream-interrupt.md +26 -34
- package/prompts/collaboration-workflow/wip-cap.md +24 -30
- package/prompts/collaboration-workflow.md +24 -25
- package/prompts/refactoring.md +33 -43
- package/prompts/review-rubric.md +84 -129
- package/prompts/review.md +111 -141
- package/prompts/sonar-hotspot-handling.md +22 -50
- package/prompts/testing-guide.md +48 -99
- package/scripts/adopt/adopt-toolkits.ts +2 -4
- package/scripts/agent/agent-exit-record.ts +23 -1
- package/scripts/agent/codex-agent-argv.ts +7 -2
- package/scripts/backlog/backlog-budget-cli.ts +1 -2
- package/scripts/backlog/backlog-drive-carried.ts +26 -0
- package/scripts/backlog/backlog-drive-cli.ts +10 -5
- package/scripts/backlog/backlog-drive-named.ts +2 -4
- package/scripts/backlog/backlog-drive-restore.ts +19 -1
- package/scripts/backlog/backlog-drive-seats.ts +27 -0
- package/scripts/backlog/backlog-drive.ts +11 -6
- package/scripts/backlog/backlog-ready.ts +13 -6
- package/scripts/behavior/behavior-rules.ts +3 -2
- package/scripts/cost-runtime/codex-usage.ts +4 -2
- package/scripts/cost-runtime/cost-cli.ts +1 -2
- package/scripts/delegation/investigation-guard.ts +1 -1
- package/scripts/delegation/investigation-reads.ts +1 -1
- package/scripts/discovery/repo-discovery.ts +4 -2
- package/scripts/discovery/repo-origin.ts +2 -4
- package/scripts/doctor/doctor-logic.ts +6 -3
- package/scripts/document/document-byte-budget.ts +27 -27
- package/scripts/document/document-byte-check.ts +1 -2
- package/scripts/document/entry-read-budget.ts +2 -2
- package/scripts/epic/epic-audit-rationale.ts +8 -2
- package/scripts/epic/epic-bundle-cli.ts +1 -2
- package/scripts/epic/epic-chains.ts +6 -2
- package/scripts/epic/epic-close-comment.ts +1 -2
- package/scripts/gate/verification-gate.ts +7 -2
- package/scripts/gh/git-pr-ai-review.ts +12 -1
- package/scripts/gh/git-pr-confirmation.ts +11 -7
- package/scripts/gh/git-pr.ts +6 -2
- package/scripts/git/git-common-directory.ts +3 -3
- package/scripts/git/git-diff-reads.ts +16 -9
- package/scripts/git/git-execa-error.ts +1 -2
- package/scripts/hooks/format-edited-file.ts +10 -4
- package/scripts/hooks/pre-push-unit.ts +15 -4
- package/scripts/hooks/pretool-guard.ts +23 -29
- package/scripts/hooks/step-zero-notice.ts +3 -2
- package/scripts/init/hook-command-rewrite.ts +6 -2
- package/scripts/init/init-logic-deploy-vps.ts +4 -2
- package/scripts/issue/behavior-change-lint.ts +12 -6
- package/scripts/issue/issue-scout-cli.ts +3 -2
- package/scripts/josh/file-map-stamp.ts +1 -3
- package/scripts/josh/hook-decision.ts +17 -2
- package/scripts/josh/josh-commands-workflow.ts +1 -1
- package/scripts/lane/lane-await.ts +3 -2
- package/scripts/lane/lane-child-invocation.ts +14 -1
- package/scripts/lane/lane-guard-policy.ts +4 -4
- package/scripts/lane/lane-ledger.ts +9 -0
- package/scripts/lane/lane-load.ts +9 -2
- package/scripts/lane/lane-measure-cli.ts +2 -0
- package/scripts/lane/lane-parked.ts +18 -0
- package/scripts/lane/lane-process-load.ts +125 -0
- package/scripts/lane/lane-process-stats.ts +93 -0
- package/scripts/lane/lane-registry.ts +8 -3
- package/scripts/lane/lane-session-broker.ts +29 -5
- package/scripts/lane/lane-spent.ts +14 -3
- package/scripts/lane/lane-stats.ts +1 -0
- package/scripts/managed-marker/managed-marker-logic.ts +1 -2
- package/scripts/notify/confirmation-options.ts +59 -0
- package/scripts/notify/notify-opt-in.ts +33 -0
- package/scripts/notify/telegram-notify.ts +25 -8
- package/scripts/notify/telegram-test-logic.ts +33 -1
- package/scripts/notify/telegram-test.ts +4 -2
- package/scripts/overrides/lockfile-overrides.ts +1 -3
- package/scripts/propagate/propagate.ts +1 -2
- package/scripts/release/release-progress.ts +3 -2
- package/scripts/report/report-lint.ts +7 -4
- package/scripts/review/review-record-cli.ts +6 -2
- package/scripts/rules/destructive-command.ts +6 -9
- package/scripts/rules/direct-filing.ts +3 -7
- package/scripts/rules/early-heartbeat.ts +7 -18
- package/scripts/rules/file-body.ts +6 -13
- package/scripts/rules/filing-cap.ts +3 -6
- package/scripts/rules/git-force.ts +5 -9
- package/scripts/rules/implementation-cut.ts +37 -35
- package/scripts/rules/index-guard.ts +4 -7
- package/scripts/rules/issue-comments.ts +5 -15
- package/scripts/rules/josh-git-bare.ts +3 -6
- package/scripts/rules/lane-background.ts +10 -21
- package/scripts/rules/lane-carry-conflict.ts +7 -17
- package/scripts/rules/lane-interactive-ask.ts +6 -11
- package/scripts/rules/lane-park.ts +5 -13
- package/scripts/rules/lane-split-park.ts +2 -7
- package/scripts/rules/lane-switch-main.ts +9 -12
- package/scripts/rules/oracle-consulted.ts +4 -5
- package/scripts/rules/piped-verification.ts +81 -31
- package/scripts/rules/poll-loop.ts +7 -17
- package/scripts/rules/pre-gate-cut.ts +7 -21
- package/scripts/rules/prior-comment-read.ts +7 -5
- package/scripts/rules/protected-files.ts +5 -9
- package/scripts/rules/raw-field-body.ts +3 -7
- package/scripts/rules/rule-body-guard.ts +4 -11
- package/scripts/rules/run-tail-rule.ts +6 -22
- package/scripts/rules/shell-body-trigger.ts +7 -8
- package/scripts/rules/shell-segments.ts +7 -0
- package/scripts/rules/stop-rules.ts +17 -29
- package/scripts/rules/test-declared-commit.ts +4 -10
- package/scripts/rules/third-party-write.ts +5 -10
- package/scripts/rules/worktree-guard.ts +5 -12
- package/scripts/run/board/run-board-github.ts +11 -3
- package/scripts/run/board/run-board-idle.ts +31 -0
- package/scripts/run/board/run-board-read.ts +9 -12
- package/scripts/run/board/run-board-reason.ts +7 -3
- package/scripts/run/board/run-board-state.ts +4 -0
- package/scripts/run/board/run-board-status.ts +18 -3
- package/scripts/run/board/run-board-thanks.ts +7 -2
- package/scripts/run/board/run-board-tick.ts +32 -7
- package/scripts/run/carry/run-carry-args.ts +1 -3
- package/scripts/run/carry/run-carry-conversation.ts +7 -2
- package/scripts/run/cut/run-cut-cli.ts +15 -20
- package/scripts/run/cut/run-cut-decline.ts +72 -0
- package/scripts/run/cut/run-cut-ready.ts +74 -0
- package/scripts/run/cut/run-cut-report.ts +13 -0
- package/scripts/run/cut/run-cut.ts +10 -1
- package/scripts/run/entry/run-entry-stop.ts +6 -3
- package/scripts/run/hold/run-hold-cli.ts +14 -3
- package/scripts/run/hold/run-hold.ts +6 -3
- package/scripts/run/merge/run-merge-cli.ts +30 -7
- package/scripts/run/merge/run-merge-ship.ts +59 -1
- package/scripts/run/merge/run-merge.ts +3 -2
- package/scripts/run/run-ending.ts +31 -3
- package/scripts/run/run-parent-cut-hook.ts +5 -10
- package/scripts/run/run-preflight.ts +1 -2
- package/scripts/run/run-stop-notify.ts +3 -2
- package/scripts/run/run-watcher-guard.ts +6 -3
- package/scripts/run/ship/run-ship-review-steps.ts +13 -4
- package/scripts/run/ship/run-ship-review.ts +15 -2
- package/scripts/run/tidy/run-tidy-cli.ts +3 -3
- package/scripts/run/tidy/run-tidy-lanes.ts +3 -2
- package/scripts/safe-chain/project-config.ts +7 -2
- package/scripts/sync/sync-hook-safety.ts +8 -2
- package/scripts/test/unit-worker-share.ts +3 -2
- package/scripts/time-runtime/time-batch-guard.ts +3 -3
- package/scripts/time-runtime/time-hook-transcript.ts +3 -2
- package/scripts/time-runtime/time-shell.ts +10 -0
- package/scripts/version/effective-upstream.ts +3 -4
- package/scripts/version/kit-version-config.ts +8 -2
- package/scripts/version/latest-corepack.ts +1 -2
- package/scripts/version/release-hold.ts +11 -4
- package/scripts/version/version-commands.ts +12 -4
- package/dist/hooks/chunk-UT7YC3N2.js +0 -56
|
@@ -178,10 +178,13 @@ function issues_traced(traced: ReadonlyArray<TracedLane>, trace: ProcessTrace):
|
|
|
178
178
|
return traced.filter(([, answer]) => answer === trace).map(([issue]) => issue)
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
function traced_of(lanes: ReadonlyArray<LaneInfo>, probes: SpentProbes): Array<TracedLane> {
|
|
182
|
+
return lanes.map((lane): TracedLane => [lane.issue, trace_of(lane, probes)])
|
|
183
|
+
}
|
|
184
|
+
|
|
181
185
|
/** The lanes in use — what a run being replaced must not be stopped over. */
|
|
182
186
|
async function lanes_in_use(probes: SpentProbes = LIVE_PROBES): Promise<LaneUse> {
|
|
183
|
-
const
|
|
184
|
-
const traced = lanes.map((lane): TracedLane => [lane.issue, trace_of(lane, probes)])
|
|
187
|
+
const traced = traced_of(await lane_registry.list_lanes(), probes)
|
|
185
188
|
|
|
186
189
|
return {
|
|
187
190
|
working: issues_traced(traced, PROCESS_ALIVE),
|
|
@@ -189,7 +192,15 @@ async function lanes_in_use(probes: SpentProbes = LIVE_PROBES): Promise<LaneUse>
|
|
|
189
192
|
}
|
|
190
193
|
}
|
|
191
194
|
|
|
192
|
-
|
|
195
|
+
/** The lanes no child is working in, read as such — never one whose probe did not answer. */
|
|
196
|
+
function idle_lanes(
|
|
197
|
+
lanes: ReadonlyArray<LaneInfo>,
|
|
198
|
+
probes: SpentProbes = LIVE_PROBES,
|
|
199
|
+
): Array<string> {
|
|
200
|
+
return issues_traced(traced_of(lanes, probes), PROCESS_NONE)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const lane_spent = { idle_lanes, kept_reason, lanes_in_use, sort_lanes }
|
|
193
204
|
|
|
194
205
|
export type { KeptLane, LaneUse, SpentProbes, SpentSort }
|
|
195
206
|
export { lane_spent }
|
|
@@ -46,8 +46,7 @@ function apply_marker_for_destination(
|
|
|
46
46
|
text: string,
|
|
47
47
|
package_name: string,
|
|
48
48
|
): string {
|
|
49
|
-
if (!is_workflow_destination(destination)) return text
|
|
50
|
-
if (is_marked(text)) return text
|
|
49
|
+
if (!is_workflow_destination(destination) || is_marked(text)) return text
|
|
51
50
|
|
|
52
51
|
return `${marker_block(package_name)}${text}`
|
|
53
52
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { session_language } from '#scripts/josh/session-language'
|
|
2
|
+
|
|
3
|
+
// **A confirmation names what to choose, not only why the run stopped.** The person reading it is
|
|
4
|
+
// off-screen by definition, so the message itself carries the recommended choice and the others —
|
|
5
|
+
// in the session language, because that is the language the person works in.
|
|
6
|
+
|
|
7
|
+
interface ConfirmationOptions {
|
|
8
|
+
recommended: string
|
|
9
|
+
alternatives: ReadonlyArray<string>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Headings {
|
|
13
|
+
recommended: string
|
|
14
|
+
alternatives: string
|
|
15
|
+
none: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const JAPANESE_LANG_PREFIX = 'ja'
|
|
19
|
+
const JAPANESE_HEADINGS: Headings = {
|
|
20
|
+
recommended: '推奨:',
|
|
21
|
+
alternatives: '他の選択肢:',
|
|
22
|
+
none: 'なし',
|
|
23
|
+
}
|
|
24
|
+
const ENGLISH_HEADINGS: Headings = {
|
|
25
|
+
recommended: 'Recommended:',
|
|
26
|
+
alternatives: 'Alternatives:',
|
|
27
|
+
none: 'none',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Only the two languages the workflow is written in have headings; any other falls back to English.
|
|
31
|
+
function headings(): Headings {
|
|
32
|
+
const { lang } = session_language.resolve_session_lang()
|
|
33
|
+
|
|
34
|
+
return lang.toLowerCase().startsWith(JAPANESE_LANG_PREFIX) ? JAPANESE_HEADINGS : ENGLISH_HEADINGS
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// An empty list is stated as none, so a confirmation with no other option says so rather than
|
|
38
|
+
// leaving a reader to wonder whether the line was lost.
|
|
39
|
+
function alternatives_block(options: ConfirmationOptions, labels: Headings): string {
|
|
40
|
+
if (options.alternatives.length === 0) return `${labels.alternatives} ${labels.none}`
|
|
41
|
+
|
|
42
|
+
const lines = options.alternatives.map((alternative) => `- ${alternative}`)
|
|
43
|
+
|
|
44
|
+
return [labels.alternatives, ...lines].join('\n')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The block is handed to the sender as its own `choices` field rather than appended to the body:
|
|
48
|
+
// the body is what a too-long message is cut from, and the choices are the one part a reader cannot
|
|
49
|
+
// act without, so they must survive the cut.
|
|
50
|
+
function block(options: ConfirmationOptions): string {
|
|
51
|
+
const labels = headings()
|
|
52
|
+
|
|
53
|
+
return `${labels.recommended} ${options.recommended}\n${alternatives_block(options, labels)}`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const confirmation_options = { block }
|
|
57
|
+
|
|
58
|
+
export { confirmation_options }
|
|
59
|
+
export type { ConfirmationOptions }
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { TelegramTaskType } from './telegram-notify'
|
|
2
|
+
|
|
3
|
+
// **`planning` and `completion` reach a person only when they asked for them.** Neither needs the
|
|
4
|
+
// person to act now — a plan is posted on the Issue and a merge is visible on the pull request — so
|
|
5
|
+
// sending both on every run buried the types that do need someone. Every other type is unaffected.
|
|
6
|
+
//
|
|
7
|
+
// **One variable listing the opted-in types, not one switch per type**, so making a further type
|
|
8
|
+
// optional later adds an entry to `OPTIONAL_TASK_TYPES` rather than another variable.
|
|
9
|
+
const OPT_IN_KEY = 'JOSH_NOTIFY_OPT_IN'
|
|
10
|
+
const OPTIONAL_TASK_TYPES: ReadonlySet<TelegramTaskType> = new Set(['planning', 'completion'])
|
|
11
|
+
const LIST_SEPARATOR = ','
|
|
12
|
+
|
|
13
|
+
function opted_in_types(): ReadonlyArray<string> {
|
|
14
|
+
return (process.env[OPT_IN_KEY] ?? '')
|
|
15
|
+
.split(LIST_SEPARATOR)
|
|
16
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
17
|
+
.filter((entry) => entry.length > 0)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Unset means none: an optional type is sent only once it is named.
|
|
21
|
+
function is_skipped(task_type: TelegramTaskType): boolean {
|
|
22
|
+
if (!OPTIONAL_TASK_TYPES.has(task_type)) return false
|
|
23
|
+
|
|
24
|
+
return !opted_in_types().includes(task_type)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function skip_line(task_type: TelegramTaskType): string {
|
|
28
|
+
return `🔕 ${task_type} notifications are not opted in (${OPT_IN_KEY}).`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const notify_opt_in = { is_skipped, skip_line }
|
|
32
|
+
|
|
33
|
+
export { notify_opt_in }
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { gh_spawn } from '#scripts/gh/gh-spawn'
|
|
2
2
|
import { timed_fetch } from '#scripts/lib/timed-fetch'
|
|
3
3
|
import { z } from 'zod'
|
|
4
|
+
import { confirmation_options, type ConfirmationOptions } from './confirmation-options'
|
|
5
|
+
import { notify_opt_in } from './notify-opt-in'
|
|
4
6
|
|
|
5
7
|
const TELEGRAM_API_BASE = 'https://api.telegram.org'
|
|
6
8
|
// `sendMessage` refuses a text longer than this.
|
|
@@ -42,6 +44,9 @@ interface TelegramSendInput {
|
|
|
42
44
|
repo_name: string | undefined
|
|
43
45
|
issue_title: string | undefined
|
|
44
46
|
body: string | undefined
|
|
47
|
+
// A confirmation's recommended choice and the others — set beside the body rather than inside it
|
|
48
|
+
// so a body cut to the length limit never takes the choices with it.
|
|
49
|
+
choices?: string | undefined
|
|
45
50
|
issue_url: string | undefined
|
|
46
51
|
pr_url: string | undefined
|
|
47
52
|
}
|
|
@@ -149,6 +154,16 @@ function is_notify_disabled(): boolean {
|
|
|
149
154
|
return parse_environment_string(process.env[NOTIFY_SWITCH_KEY]).toLowerCase() === NOTIFY_OFF_VALUE
|
|
150
155
|
}
|
|
151
156
|
|
|
157
|
+
// The line a skipped send prints, or `undefined` for a send that goes out. The blanket opt-out wins
|
|
158
|
+
// over the per-type opt-in, so `off` stays the one switch that silences everything.
|
|
159
|
+
function skip_line(task_type: TelegramTaskType): string | undefined {
|
|
160
|
+
if (is_notify_disabled()) {
|
|
161
|
+
return `🔕 Telegram notifications are disabled (${NOTIFY_SWITCH_KEY}=${NOTIFY_OFF_VALUE}).`
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return notify_opt_in.is_skipped(task_type) ? notify_opt_in.skip_line(task_type) : undefined
|
|
165
|
+
}
|
|
166
|
+
|
|
152
167
|
function build_header(task_type: TelegramTaskType, repo_name: string | undefined): string {
|
|
153
168
|
const { icon, label } = TASK_DEFINITIONS[task_type]
|
|
154
169
|
if (repo_name === undefined || repo_name.length === 0) return `${icon} ${label}`
|
|
@@ -185,6 +200,7 @@ function build_blocks(input: TelegramSendInput): Array<string> {
|
|
|
185
200
|
const blocks: Array<string> = [build_title_block(input)]
|
|
186
201
|
|
|
187
202
|
push_if_present(blocks, input.body)
|
|
203
|
+
push_if_present(blocks, input.choices)
|
|
188
204
|
blocks.push(...build_url_parts(input))
|
|
189
205
|
|
|
190
206
|
return blocks
|
|
@@ -243,13 +259,13 @@ function build_send_failure(error: unknown, config: TelegramConfig): Error {
|
|
|
243
259
|
//
|
|
244
260
|
// The failure it throws carries a `cause`, and that cause is redacted too — see `build_send_failure`.
|
|
245
261
|
//
|
|
246
|
-
// The opt-out
|
|
247
|
-
// skips the same way and none has to repeat the check.
|
|
262
|
+
// The opt-out and the per-type opt-in are checked here, before `load_config`, so every caller — the
|
|
263
|
+
// tolerant form included — skips the same way and none has to repeat the check.
|
|
248
264
|
async function send(input: TelegramSendInput): Promise<void> {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
)
|
|
265
|
+
const skipped = skip_line(input.task_type)
|
|
266
|
+
|
|
267
|
+
if (skipped !== undefined) {
|
|
268
|
+
console.info(skipped)
|
|
253
269
|
|
|
254
270
|
return
|
|
255
271
|
}
|
|
@@ -341,7 +357,7 @@ async function warn(input: WarningInput): Promise<boolean> {
|
|
|
341
357
|
)
|
|
342
358
|
}
|
|
343
359
|
|
|
344
|
-
interface ConfirmInput {
|
|
360
|
+
interface ConfirmInput extends ConfirmationOptions {
|
|
345
361
|
issue_title: string
|
|
346
362
|
body: string
|
|
347
363
|
recovery: string
|
|
@@ -365,6 +381,7 @@ async function confirm(input: ConfirmInput): Promise<boolean> {
|
|
|
365
381
|
repo_name: gh_spawn.get_repo_name_with_owner_within(REPO_LOOKUP_TIMEOUT_MS),
|
|
366
382
|
issue_title: input.issue_title,
|
|
367
383
|
body: input.body,
|
|
384
|
+
choices: confirmation_options.block(input),
|
|
368
385
|
issue_url: undefined,
|
|
369
386
|
pr_url: undefined,
|
|
370
387
|
},
|
|
@@ -463,4 +480,4 @@ const telegram_notify = {
|
|
|
463
480
|
}
|
|
464
481
|
|
|
465
482
|
export { telegram_notify, build_text, TASK_DEFINITIONS, telegram_environment_schema }
|
|
466
|
-
export type { TelegramSendInput, TelegramTaskType }
|
|
483
|
+
export type { ConfirmInput, TelegramSendInput, TelegramTaskType }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cli_body } from '#scripts/josh/cli-body'
|
|
2
|
+
import { confirmation_options } from './confirmation-options'
|
|
2
3
|
import type { TelegramSendInput, TelegramTaskType } from './telegram-notify'
|
|
3
4
|
|
|
4
5
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
@@ -10,6 +11,8 @@ interface CliValues {
|
|
|
10
11
|
'body-file'?: string
|
|
11
12
|
'issue-url'?: string
|
|
12
13
|
'pr-url'?: string
|
|
14
|
+
recommended?: string
|
|
15
|
+
alternative?: Array<string>
|
|
13
16
|
}
|
|
14
17
|
/* eslint-enable @typescript-eslint/naming-convention */
|
|
15
18
|
|
|
@@ -67,12 +70,41 @@ function normalize_body(values: CliValues): string | undefined {
|
|
|
67
70
|
})
|
|
68
71
|
}
|
|
69
72
|
|
|
73
|
+
// **A confirmation without a recommended choice is refused, not sent**: the person it reaches is
|
|
74
|
+
// off-screen, and a stop that names no choice leaves them to reconstruct one. `--alternative` may be
|
|
75
|
+
// absent — a stop with one way forward says so — so only `--recommended` is required.
|
|
76
|
+
function require_recommended(values: CliValues): string {
|
|
77
|
+
const recommended = values.recommended?.trim() ?? ''
|
|
78
|
+
|
|
79
|
+
if (recommended.length === 0) {
|
|
80
|
+
throw new Error('--task-type confirmation needs --recommended "<the recommended choice>".')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return recommended
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function given_alternatives(values: CliValues): Array<string> {
|
|
87
|
+
return (values.alternative ?? []).filter((alternative) => alternative.trim().length > 0)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function build_choices(values: CliValues, task_type: TelegramTaskType): string | undefined {
|
|
91
|
+
if (task_type !== 'confirmation') return undefined
|
|
92
|
+
|
|
93
|
+
return confirmation_options.block({
|
|
94
|
+
recommended: require_recommended(values),
|
|
95
|
+
alternatives: given_alternatives(values),
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
70
99
|
function build_input(input: { values: CliValues; context: ResolvedContext }): TelegramSendInput {
|
|
100
|
+
const task_type = parse_task_type(input.values['task-type'])
|
|
101
|
+
|
|
71
102
|
return {
|
|
72
|
-
task_type
|
|
103
|
+
task_type,
|
|
73
104
|
repo_name: coalesce(input.values['repo-name'], input.context.repo_name),
|
|
74
105
|
issue_title: coalesce(input.values['issue-title'], input.context.issue_title),
|
|
75
106
|
body: normalize_body(input.values),
|
|
107
|
+
choices: build_choices(input.values, task_type),
|
|
76
108
|
issue_url: input.values['issue-url'],
|
|
77
109
|
pr_url: input.values['pr-url'],
|
|
78
110
|
}
|
|
@@ -14,10 +14,10 @@ const REPO_NAME_SEPARATOR = '/'
|
|
|
14
14
|
const ARGV_OFFSET = 2
|
|
15
15
|
const FAILURE_EXIT_CODE = 1
|
|
16
16
|
const USAGE =
|
|
17
|
-
'Usage: josh notify [--task-type <type>] [--body <text> | --body-file <path>] [--issue-url <url>] [--pr-url <url>] [--issue-title <text>] [--repo-name <name>]'
|
|
17
|
+
'Usage: josh notify [--task-type <type>] [--body <text> | --body-file <path>] [--issue-url <url>] [--pr-url <url>] [--issue-title <text>] [--repo-name <name>] [--recommended <text>] [--alternative <text>]...'
|
|
18
18
|
|
|
19
19
|
// The free-text values a caller writes, often opening with a Markdown `-` bullet.
|
|
20
|
-
const FREE_TEXT_FLAGS = ['--body', '--issue-title']
|
|
20
|
+
const FREE_TEXT_FLAGS = ['--body', '--issue-title', '--recommended', '--alternative']
|
|
21
21
|
|
|
22
22
|
const OPTIONS = {
|
|
23
23
|
'task-type': { type: 'string' },
|
|
@@ -27,6 +27,8 @@ const OPTIONS = {
|
|
|
27
27
|
'body-file': { type: 'string' },
|
|
28
28
|
'issue-url': { type: 'string' },
|
|
29
29
|
'pr-url': { type: 'string' },
|
|
30
|
+
recommended: { type: 'string' },
|
|
31
|
+
alternative: { type: 'string', multiple: true },
|
|
30
32
|
} as const
|
|
31
33
|
|
|
32
34
|
// `undefined` for a line nobody can read — an unknown flag, or a flag given no value.
|
|
@@ -42,9 +42,7 @@ function find_entry_mismatch(
|
|
|
42
42
|
specifier: string,
|
|
43
43
|
override: string | undefined,
|
|
44
44
|
): Array<SpecifierMismatch> {
|
|
45
|
-
if (override === undefined) return []
|
|
46
|
-
if (is_non_registry(specifier)) return []
|
|
47
|
-
if (specifier === override) return []
|
|
45
|
+
if (override === undefined || specifier === override || is_non_registry(specifier)) return []
|
|
48
46
|
|
|
49
47
|
return [{ importer, name, lockfile_specifier: specifier, override }]
|
|
50
48
|
}
|
|
@@ -277,8 +277,7 @@ async function run(argv: ReadonlyArray<string>): Promise<number> {
|
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
const plan = plan_run(options)
|
|
280
|
-
if (plan === undefined) return FAILURE_EXIT_CODE
|
|
281
|
-
if (!(await resolve_publish(plan, options))) return FAILURE_EXIT_CODE
|
|
280
|
+
if (plan === undefined || !(await resolve_publish(plan, options))) return FAILURE_EXIT_CODE
|
|
282
281
|
|
|
283
282
|
return propagate_to_consumers(plan, options.is_dry_run)
|
|
284
283
|
}
|
|
@@ -145,8 +145,9 @@ async function wait_for_github_release(version: string): Promise<boolean> {
|
|
|
145
145
|
async function wait_for_distribution(version: string): Promise<number> {
|
|
146
146
|
const manifest = read_manifest(process.cwd())
|
|
147
147
|
|
|
148
|
-
if (!(await wait_for_npm(manifest, version)))
|
|
149
|
-
|
|
148
|
+
if (!(await wait_for_npm(manifest, version)) || !(await wait_for_github_release(version))) {
|
|
149
|
+
return FAILURE_EXIT_CODE
|
|
150
|
+
}
|
|
150
151
|
|
|
151
152
|
console.info(complete_line(version))
|
|
152
153
|
|
|
@@ -102,8 +102,7 @@ function overview_lines(summary: string): ReadonlyArray<string> {
|
|
|
102
102
|
function item_sentence(line: string): string {
|
|
103
103
|
const open = line.indexOf(BOLD_MARKER)
|
|
104
104
|
const close = open === -1 ? -1 : line.indexOf(BOLD_MARKER, open + BOLD_MARKER.length)
|
|
105
|
-
const raw =
|
|
106
|
-
close === -1 ? line.slice(ITEM_LINE_PREFIX.length) : line.slice(close + BOLD_MARKER.length)
|
|
105
|
+
const raw = line.slice(close === -1 ? ITEM_LINE_PREFIX.length : close + BOLD_MARKER.length)
|
|
107
106
|
|
|
108
107
|
return raw.replace(LEADING_LABEL_PUNCTUATION, '').trim()
|
|
109
108
|
}
|
|
@@ -199,8 +198,12 @@ function break_condition_count(lines: ReadonlyArray<string>): number {
|
|
|
199
198
|
}
|
|
200
199
|
|
|
201
200
|
function case_line_violations(summary: string): ReadonlyArray<string> {
|
|
202
|
-
if (
|
|
203
|
-
|
|
201
|
+
if (
|
|
202
|
+
!has_changes_section(summary) ||
|
|
203
|
+
changes_region_lines(summary).some((line) => line.includes(CASE_LABEL))
|
|
204
|
+
) {
|
|
205
|
+
return []
|
|
206
|
+
}
|
|
204
207
|
|
|
205
208
|
return [`✖ no case tier (${CASE_LABEL}:) in ${CHANGES_LABEL}`]
|
|
206
209
|
}
|
|
@@ -58,8 +58,12 @@ interface Parsed {
|
|
|
58
58
|
// A finding spec is `<category>:<severity>:<file>`; the file field keeps any `:line` a citation
|
|
59
59
|
// carries, so only the first two colons separate fields.
|
|
60
60
|
function to_finding(category: string, severity: string, file: string): Finding | undefined {
|
|
61
|
-
if (
|
|
62
|
-
|
|
61
|
+
if (
|
|
62
|
+
!review_finding_ledger.is_category(category) ||
|
|
63
|
+
!review_finding_ledger.is_severity(severity)
|
|
64
|
+
) {
|
|
65
|
+
return undefined
|
|
66
|
+
}
|
|
63
67
|
|
|
64
68
|
return review_finding_ledger.is_valid_file(file) ? { category, severity, file } : undefined
|
|
65
69
|
}
|
|
@@ -97,21 +97,18 @@ function is_destructive_command(command: string): boolean {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
const DESTRUCTIVE_COMMAND_REASON =
|
|
100
|
-
'⛔ destructive command: a recursive forced `rm
|
|
101
|
-
'
|
|
102
|
-
'
|
|
103
|
-
'
|
|
104
|
-
'The `.claude/settings.json` deny list matches prefix globs, so it misses these spellings; this row ' +
|
|
105
|
-
'reads the argv instead. Remove a file or directory one path at a time without `-f`, or ask the user ' +
|
|
106
|
-
'to run the command in their own terminal. `gh issue close` and removing a label from an Issue ' +
|
|
107
|
-
'(`DELETE repos/<o>/<r>/issues/<N>/labels/<name>`) are not refused. **This rule fires on every ' +
|
|
108
|
-
'occurrence, not once per run.**'
|
|
100
|
+
'⛔ destructive command: a recursive forced `rm`, a `gh repo delete` / `archive` / visibility edit, ' +
|
|
101
|
+
'`gh pr close` or a `gh api -X DELETE` cannot be undone, which is Tier C. Remove one path at a ' +
|
|
102
|
+
'time without `-f`, or ask the user to run it. It fires on every occurrence ' +
|
|
103
|
+
'(`prompts/collaboration-workflow/operating-rules.md`).'
|
|
109
104
|
|
|
110
105
|
const ROW = {
|
|
111
106
|
id: 'destructive-command',
|
|
112
107
|
is_trigger: bash_triggers.on_bash_command(is_destructive_command),
|
|
113
108
|
reason: DESTRUCTIVE_COMMAND_REASON,
|
|
114
109
|
decide: (): boolean => true,
|
|
110
|
+
// The opening the reason carried before it was cut to the action and the next command.
|
|
111
|
+
former_reasons: ['⛔ destructive command: a recursive forced `rm` (`rm -rf`'],
|
|
115
112
|
}
|
|
116
113
|
|
|
117
114
|
const destructive_command = { DESTRUCTIVE_COMMAND_REASON, ROW, is_destructive_command }
|
|
@@ -9,13 +9,9 @@ import { bash_triggers } from './bash-triggers'
|
|
|
9
9
|
|
|
10
10
|
const DIRECT_FILING_REASON =
|
|
11
11
|
'⛔ direct filing: an Issue is filed with `pnpm josh issue:file "<title>" --body-file <path> ' +
|
|
12
|
-
'--depth <0|1|2
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'into another repository, and `epic:bundle` after the create — so none of them is a step to remember. ' +
|
|
16
|
-
'When the scout prints candidates it holds the filing: read each, then reissue with `--distinct <N,…>` ' +
|
|
17
|
-
'for the ones that are separate deliverables. Reference: `docs/josh-commands-backlog.md` → "`josh issue:file`". ' +
|
|
18
|
-
'**This rule fires on every occurrence, not once per run.**'
|
|
12
|
+
'--depth <0|1|2>`, never with `gh api …/issues` or `gh issue create` — it runs the scout, the lint, ' +
|
|
13
|
+
'the labels and `epic:bundle` itself. It fires on every occurrence (`docs/josh-commands-backlog.md` ' +
|
|
14
|
+
'→ "`josh issue:file`").'
|
|
19
15
|
|
|
20
16
|
function is_any_filing(command: string): boolean {
|
|
21
17
|
return bash_triggers.is_direct_filing(command) || bash_triggers.is_issue_filing(command)
|
|
@@ -178,25 +178,14 @@ function decide(call: GuardedCall, run: GuardRun, can_record: boolean): boolean
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
// The instruction in the shape a refusal can carry: what the call is about to do, what already does it
|
|
181
|
-
// properly, and the one command an explicit ask is answered with. The observed failure is
|
|
182
|
-
//
|
|
181
|
+
// properly, and the one command an explicit ask is answered with. The observed failure behind it is in
|
|
182
|
+
// `docs/maintainers/rule-delivery-rationale.md`.
|
|
183
183
|
const EARLY_HEARTBEAT_REASON =
|
|
184
|
-
'⛔ early heartbeat: this call arms a wait timer of its own
|
|
185
|
-
|
|
186
|
-
'
|
|
187
|
-
'
|
|
188
|
-
|
|
189
|
-
'after it exits, which it does when newly runnable work arrives. Two timers armed at ' +
|
|
190
|
-
'once is what produced reports 3–5 minutes apart on a 15-minute setting ' +
|
|
191
|
-
'(joshuafolkken/kit#1570): a new one was armed on the turn a timer fired and again on the turn a ' +
|
|
192
|
-
"child's completion woke the run, and `--mark` recorded each report without anything refusing the " +
|
|
193
|
-
'early ones. **An explicit ask is not a heartbeat**: if the person asked for progress now, run ' +
|
|
194
|
-
'`pnpm josh run:progress --once`, which prints the five labelled lines whatever the clock says — ' +
|
|
195
|
-
'under `backlogrun`, `pnpm josh run:board --chat` instead. If you are ' +
|
|
196
|
-
'waiting on something rather than on a clock, wait on the thing itself — `pnpm josh followup` ' +
|
|
197
|
-
'waits for CI. The procedure is `.claude/skills/workflow-commands/backlogrun.md` → "Progress while ' +
|
|
198
|
-
'the run is quiet". **This rule fires on every early arm, not once per run**, so reissuing the ' +
|
|
199
|
-
'same sleep will be refused again.'
|
|
184
|
+
'⛔ early heartbeat: this call arms a wait timer of its own. Start `pnpm josh run:progress --wait` ' +
|
|
185
|
+
"once in the background: it reports to the run's event stream on its own — relay nothing. When " +
|
|
186
|
+
'asked, `pnpm josh run:progress --once` — under `backlogrun`, `pnpm josh run:board --chat`; ' +
|
|
187
|
+
'for CI, `pnpm josh followup`. It fires on every occurrence ' +
|
|
188
|
+
'(`.claude/skills/workflow-commands/progress-watcher.md`).'
|
|
200
189
|
|
|
201
190
|
// **Keeping this rule is letting the watcher hold the clock** — `pnpm josh run:progress --wait` waits
|
|
202
191
|
// the interval out, and `--once` answers an explicit ask. `--mark` is neither: it records a report
|
|
@@ -171,20 +171,13 @@ function carries_a_file_body(
|
|
|
171
171
|
)
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
// The instruction in the shape a refusal can carry: what the command is doing
|
|
175
|
-
//
|
|
176
|
-
// surprising — the command body is billed on every later request of the run.
|
|
174
|
+
// The instruction in the shape a refusal can carry: what the command is doing and the safe tool. The
|
|
175
|
+
// measured cost behind it is in `file-edits.md`.
|
|
177
176
|
const FILE_BODY_REASON =
|
|
178
|
-
"⛔ file body in a shell command: this carries a file's new text inline
|
|
179
|
-
'
|
|
180
|
-
'
|
|
181
|
-
'
|
|
182
|
-
'is billed for the rest of it (`file-edits.md` measured one run at ~2m45s spent retyping existing ' +
|
|
183
|
-
'lines). Edit a region with the Edit tool (old / new); a change scattered so widely that the ' +
|
|
184
|
-
'partial edits would carry more than the file is rewritten whole with the Write tool, never a ' +
|
|
185
|
-
'heredoc. Pass a body by path (`--field body=@<path>`, `--body-file <path>`). A new-file creation (`cat > ' +
|
|
186
|
-
'<new-file> <<EOF`), a read-only heredoc (`cat <<EOF`), and a short `sed -i` stay allowed. **This ' +
|
|
187
|
-
'rule fires on every occurrence, not once per run.**'
|
|
177
|
+
"⛔ file body in a shell command: this carries a file's new text inline, which is re-read on every " +
|
|
178
|
+
'later request. Edit a region with the Edit tool, rewrite a whole file with the Write tool, and ' +
|
|
179
|
+
'pass a body by path (`--body-file <path>`); a new-file `cat > <new-file> <<EOF` stays allowed. ' +
|
|
180
|
+
'This rule fires on every occurrence (`prompts/collaboration-workflow/file-edits.md`).'
|
|
188
181
|
|
|
189
182
|
const ROW = {
|
|
190
183
|
id: 'file-body',
|
|
@@ -104,12 +104,9 @@ function decide(_call: unknown, run: GuardRun): boolean {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
const FILING_CAP_REASON =
|
|
107
|
-
`⛔ filing cap: this run has already filed ${String(FILING_CAP)} Issues, the per-run ceiling
|
|
108
|
-
|
|
109
|
-
'
|
|
110
|
-
"`prerequisite.md`, `observation-filing.md`). A further Issue that genuinely must exist is a person's call, not " +
|
|
111
|
-
"this run's. **This rule fires on every filing past the cap, not once per run**, so reissuing the " +
|
|
112
|
-
'same filing will be refused again.'
|
|
107
|
+
`⛔ filing cap: this run has already filed ${String(FILING_CAP)} Issues, the per-run ceiling. Stop ` +
|
|
108
|
+
"filing and report; a further Issue is a person's call. It fires on every occurrence " +
|
|
109
|
+
'(`.claude/skills/workflow-commands/observation-filing.md`).'
|
|
113
110
|
|
|
114
111
|
// The enumeration row itself, so `delivered-rules.ts` spreads one entry rather than restating the
|
|
115
112
|
// trigger and reason it already single-sources here. It declares no `keeps`: staying under a cap is
|
|
@@ -69,19 +69,15 @@ function is_force_or_delete(command: string): boolean {
|
|
|
69
69
|
return shell_segments.segments_of(command).some((segment) => is_destructive_segment(segment))
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
// The instruction in the shape a refusal can carry: what the command does
|
|
73
|
-
//
|
|
72
|
+
// The instruction in the shape a refusal can carry: what the command does and the one route left. The
|
|
73
|
+
// spellings the deny glob misses are pinned on `is_force_or_delete`, not repeated in the text. There is no "safe reissue" the way a backgrounded push is for
|
|
74
74
|
// `run-tail` — a force push and a branch delete are Tier C, so the honest next move is a person's, in
|
|
75
75
|
// their own terminal.
|
|
76
76
|
const GIT_FORCE_REASON =
|
|
77
77
|
'⛔ force push or branch delete: this rewrites or removes shared history, which is Tier C and needs ' +
|
|
78
|
-
'explicit current-turn user instruction
|
|
79
|
-
'
|
|
80
|
-
'
|
|
81
|
-
'<path>` prefix, and the colon-form delete (`git push origin :branch`) all pass it. This row reads ' +
|
|
82
|
-
'the argv instead, so the spelling does not matter. For an ordinary push use `pnpm josh git`; a ' +
|
|
83
|
-
"force push, a `git push --delete` / `:branch`, or a `git branch -d` / `-D` is the user's to run in " +
|
|
84
|
-
'their own terminal. **This rule fires on every occurrence, not once per run.**'
|
|
78
|
+
'explicit current-turn user instruction. Push with `pnpm josh git`; leave a force push or branch ' +
|
|
79
|
+
"delete to the user's own terminal. This rule fires on every occurrence " +
|
|
80
|
+
'(`prompts/collaboration-workflow/operating-rules.md`).'
|
|
85
81
|
|
|
86
82
|
// The row itself, so `delivered-rules.ts` spreads one entry. `decide` returns true so it refuses every
|
|
87
83
|
// occurrence; it declares no `keeps` — not force-pushing is the absence of a call, not a call, so the
|