@joshuafolkken/kit 1.1471.0 → 1.1514.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 +6 -6
- package/.claude/skills/epic-commands/SKILL.md +2 -14
- package/.claude/skills/epic-commands/epic-bundle.md +9 -5
- package/.claude/skills/epic-commands/execution-waves.md +1 -2
- package/.claude/skills/verify-ui/SKILL.md +1 -2
- package/.claude/skills/workflow-commands/SKILL.md +7 -9
- package/.claude/skills/workflow-commands/background-commands.md +3 -7
- package/.claude/skills/workflow-commands/backlogrun-child.md +3 -10
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +6 -15
- package/.claude/skills/workflow-commands/backlogrun-park.md +2 -8
- package/.claude/skills/workflow-commands/backlogrun-progress.md +10 -19
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +8 -8
- package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -10
- package/.claude/skills/workflow-commands/backlogrun.md +31 -50
- package/.claude/skills/workflow-commands/chain-rule.md +7 -6
- package/.claude/skills/workflow-commands/delegation.md +12 -8
- package/.claude/skills/workflow-commands/entry-sequence.md +1 -3
- package/.claude/skills/workflow-commands/followup.md +3 -6
- package/.claude/skills/workflow-commands/fullrun-steps.md +3 -5
- package/.claude/skills/workflow-commands/fullrun.md +4 -6
- package/.claude/skills/workflow-commands/halfrun.md +3 -7
- package/.claude/skills/workflow-commands/into-target.md +1 -2
- package/.claude/skills/workflow-commands/issue-comments.md +3 -3
- package/.claude/skills/workflow-commands/issue-scout.md +3 -2
- package/.claude/skills/workflow-commands/latest-gate.md +2 -6
- package/.claude/skills/workflow-commands/needs-human-review.md +2 -5
- package/.claude/skills/workflow-commands/observation-filing.md +0 -14
- package/.claude/skills/workflow-commands/observation-ledger.md +4 -9
- package/.claude/skills/workflow-commands/pre-gate-cut.md +11 -16
- package/.claude/skills/workflow-commands/prerequisite.md +1 -2
- package/.claude/skills/workflow-commands/progress-watcher.md +2 -7
- package/.claude/skills/workflow-commands/prrun.md +3 -5
- package/.claude/skills/workflow-commands/retrospective.md +4 -4
- package/.claude/skills/workflow-commands/split-assessment.md +1 -3
- package/.claude/skills/workflow-commands/target-repository.md +1 -2
- package/.claude/skills/workflow-commands/working-tree-hold.md +2 -4
- package/.codex/hooks.json +6 -6
- package/.github/actions/setup-node/action.yml +1 -1
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/publish.yml +2 -2
- package/CLAUDE.md +1 -1
- package/cspell/index.yaml +7 -0
- package/dist/CLAUDE.md +1 -1
- package/dist/commands/document-section-cli.js +67 -34
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/chunk-4KKD2UTK.js +66 -0
- package/dist/hooks/{chunk-J47HU5H5.js → chunk-6TS2JD6D.js} +3 -3
- package/dist/hooks/chunk-7DSBJVFH.js +50 -0
- package/dist/hooks/{chunk-YHBZAJSV.js → chunk-A65Y4YOR.js} +3 -3
- package/dist/hooks/{chunk-E7OIKX5Z.js → chunk-APUO6U4J.js} +85 -7
- package/dist/hooks/{chunk-IX6TTTI6.js → chunk-EFXE6C37.js} +8 -22
- package/dist/hooks/chunk-GP4IEC4O.js +20 -0
- package/dist/hooks/{chunk-SHGPANFB.js → chunk-JP45SOCJ.js} +3 -3
- package/dist/hooks/{chunk-K4EOONYM.js → chunk-LAOV7NLM.js} +1 -1
- package/dist/hooks/chunk-M4HGZV7U.js +83 -0
- package/dist/hooks/{chunk-KC5AK2GF.js → chunk-O6GDRNJB.js} +3 -3
- package/dist/hooks/{chunk-YYPI7SJZ.js → chunk-PF2NZL2A.js} +5 -5
- package/dist/hooks/{chunk-H5HVWYXK.js → chunk-PTICX4OL.js} +3 -4
- package/dist/hooks/{chunk-IIGFYXKC.js → chunk-RT27QRJW.js} +581 -1438
- package/dist/hooks/{chunk-43ZHT22P.js → chunk-SYOTDYCI.js} +1127 -1089
- package/dist/hooks/{chunk-AS74OU3D.js → chunk-TNUMY6NL.js} +1507 -1336
- package/dist/hooks/chunk-UPUZF3AN.js +279 -0
- package/dist/hooks/{chunk-EUMACUK2.js → chunk-WK6SMBIC.js} +1948 -806
- package/dist/hooks/codex-hook-adapter.js +17 -14
- package/dist/hooks/duplicate-read-guard.js +7 -7
- package/dist/hooks/format-edited.js +5 -4
- package/dist/hooks/inputs.json +317 -264
- package/dist/hooks/investigation-guard.js +8 -11
- package/dist/hooks/pretool-guard.js +16 -13
- package/dist/hooks/session-lang.js +370 -25
- package/dist/hooks/stop-guard.js +2576 -176
- package/dist/josh.js +2124 -277
- package/lefthook/base.yml +5 -3
- package/package.json +12 -10
- package/prompts/collaboration-workflow/file-edits.md +1 -3
- package/prompts/collaboration-workflow/gh-rest.md +1 -2
- package/prompts/collaboration-workflow/issue-citation.md +2 -4
- package/prompts/collaboration-workflow/issue-template.md +10 -2
- package/prompts/collaboration-workflow/lane-instructions.md +35 -0
- package/prompts/collaboration-workflow/operating-rules.md +2 -6
- package/prompts/collaboration-workflow/output-bounds.md +3 -3
- package/prompts/collaboration-workflow/overview.md +1 -1
- package/prompts/collaboration-workflow/principles.md +14 -8
- package/prompts/collaboration-workflow/report-format.md +9 -0
- package/prompts/collaboration-workflow/residency.md +12 -8
- package/prompts/collaboration-workflow/rule-delivery.md +3 -11
- package/prompts/collaboration-workflow/shell-body.md +6 -6
- package/prompts/collaboration-workflow/turn-batching.md +5 -5
- package/prompts/collaboration-workflow/upstream-interrupt.md +2 -2
- package/prompts/collaboration-workflow/wip-cap.md +6 -6
- package/prompts/collaboration-workflow.md +1 -0
- package/prompts/review.md +4 -5
- package/prompts/testing-guide.md +2 -2
- package/scripts/agent/agent-argv.ts +20 -4
- package/scripts/agent/agent-role-profile.ts +101 -45
- package/scripts/agent/agent-safe-text.ts +60 -0
- package/scripts/agent/claude-agent-argv.ts +14 -0
- package/scripts/agent/codex-agent-argv.ts +2 -0
- package/scripts/agent/lane-instructions.ts +144 -0
- package/scripts/backlog/backlog-budget.ts +4 -4
- package/scripts/backlog/backlog-drive-named.ts +1 -1
- package/scripts/backlog/backlog-drive-restore.ts +9 -1
- package/scripts/backlog/backlog-occupied.ts +56 -0
- package/scripts/backlog/backlog-ready.ts +47 -10
- package/scripts/delegation/duplicate-read-guard.ts +2 -2
- package/scripts/delegation/investigation-guard.ts +15 -65
- package/scripts/delegation/investigation-reads.ts +0 -1
- package/scripts/document/document-byte-budget.ts +6 -5
- package/scripts/document/document-section-cli.ts +17 -5
- package/scripts/document/entry-read-budget.ts +2 -2
- package/scripts/document/read-set-cli.ts +4 -1
- package/scripts/epic/epic-audit-orphans.ts +5 -8
- package/scripts/epic/epic-audit.ts +52 -1
- package/scripts/epic/epic-bundle-evidence.ts +49 -26
- package/scripts/epic/epic-bundle-referenced.ts +14 -3
- package/scripts/epic/epic-bundle.ts +17 -9
- package/scripts/followup/git-followup-workflow.ts +19 -1
- package/scripts/gate/check-inputs.ts +129 -0
- package/scripts/gate/check-reuse.ts +162 -0
- package/scripts/gate/core-budget.ts +93 -44
- package/scripts/gate/core-wait.ts +130 -0
- package/scripts/gate/free-cores.ts +49 -0
- package/scripts/gate/gate-ledger.ts +17 -3
- package/scripts/gate/gate-plan.ts +23 -1
- package/scripts/gate/gate-report.ts +15 -2
- package/scripts/gate/gate-run-steps.ts +150 -0
- package/scripts/gate/held-mark.ts +55 -0
- package/scripts/gate/machine-capacity.ts +70 -11
- package/scripts/gate/pre-commit-type-check.ts +14 -1
- package/scripts/gate/verification-gate.ts +58 -125
- package/scripts/gh/git-gh-issue-rest.ts +1 -0
- package/scripts/gh/git-gh-issue-write.ts +24 -2
- package/scripts/gh/git-pr.ts +13 -1
- package/scripts/git/main-merge-regenerate.ts +78 -0
- package/scripts/git/main-merge.ts +14 -5
- package/scripts/hooks/batch-guard.ts +5 -8
- package/scripts/hooks/pre-commit-lint.ts +91 -0
- package/scripts/hooks/pre-push-unit.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +27 -52
- package/scripts/hooks/reserved-run.ts +1 -0
- package/scripts/hooks/run-hook.sh +10 -12
- package/scripts/hooks/stop-actions.ts +204 -0
- package/scripts/hooks/stop-guard.ts +62 -21
- package/scripts/init/hook-launch.ts +12 -7
- package/scripts/issue/expected-effect-lint.ts +59 -0
- package/scripts/issue/issue-cite-cli.ts +35 -15
- package/scripts/issue/issue-classification.ts +1 -1
- package/scripts/issue/issue-lint-cli.ts +5 -3
- package/scripts/issue/issue-scout-cli.ts +19 -14
- package/scripts/issue/markdown-section.ts +19 -5
- package/scripts/josh/command-usage.ts +110 -0
- package/scripts/josh/josh-command-map.ts +2 -0
- package/scripts/josh/josh-command-types.ts +55 -9
- package/scripts/josh/josh-commands-ai.ts +13 -0
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-hooks.ts +6 -0
- package/scripts/josh/josh-commands-lane.ts +10 -1
- package/scripts/josh/josh-commands-workflow.ts +5 -0
- package/scripts/josh/josh-logic.ts +6 -15
- package/scripts/josh/josh-usage-automation.ts +121 -0
- package/scripts/josh/josh-usage-backlog.ts +180 -0
- package/scripts/josh/josh-usage-developer.ts +230 -0
- package/scripts/josh/josh-usage-hooks.ts +38 -0
- package/scripts/josh/josh-usage-issue.ts +205 -0
- package/scripts/josh/josh-usage-review.ts +101 -0
- package/scripts/josh/josh-usage-run.ts +252 -0
- package/scripts/josh/josh-usage-session.ts +238 -0
- package/scripts/josh/josh-usage-workflow.ts +212 -0
- package/scripts/josh/josh.ts +14 -5
- package/scripts/josh/package-directory.ts +20 -0
- package/scripts/lane/lane-capacity.ts +8 -2
- package/scripts/lane/lane-check-stats.ts +56 -0
- package/scripts/lane/lane-child-invocation.ts +35 -4
- package/scripts/lane/lane-dispatch.ts +3 -3
- package/scripts/lane/lane-guard-policy.ts +2 -2
- package/scripts/lane/lane-handoff.ts +8 -1
- package/scripts/lane/lane-install.ts +12 -4
- package/scripts/lane/lane-ledger.ts +43 -1
- package/scripts/lane/lane-limit-cli.ts +8 -9
- package/scripts/lane/lane-measure-cli.ts +2 -0
- package/scripts/lane/lane-preserve.ts +29 -1
- package/scripts/lane/lane-registry.ts +9 -7
- package/scripts/lane/lane-restart.ts +5 -26
- package/scripts/lane/lane-session-action.ts +146 -0
- package/scripts/lane/lane-session-broker.ts +57 -138
- package/scripts/lane/lane-withdraw-cli.ts +71 -0
- package/scripts/lane/lane-withdraw.ts +182 -0
- package/scripts/lint/format.ts +4 -4
- package/scripts/lint/lint-parallel.ts +10 -3
- package/scripts/lint/lint-related-scope.ts +4 -1
- package/scripts/notify/telegram-test.ts +10 -5
- package/scripts/report/report-format-reference.ts +2 -0
- package/scripts/review/acceptance-map-check.ts +61 -0
- package/scripts/review/acceptance-map.ts +205 -0
- package/scripts/review/review-brief-cli.ts +1 -1
- package/scripts/rules/agent-model.ts +59 -0
- package/scripts/rules/delivered-rules.ts +11 -2
- package/scripts/rules/filing-cap.ts +1 -0
- package/scripts/rules/firing-point.ts +6 -5
- package/scripts/rules/guard-remedy.ts +117 -0
- package/scripts/rules/issue-comments.ts +67 -6
- package/scripts/rules/last-prompt.ts +50 -5
- package/scripts/rules/oracle-list-cli.ts +7 -0
- package/scripts/rules/piped-verification.ts +3 -1
- package/scripts/rules/rule-list.ts +4 -1
- package/scripts/rules/rule-registry-rows.ts +10 -0
- package/scripts/rules/rule-registry.ts +7 -4
- package/scripts/rules/rule-rewrite.ts +114 -0
- package/scripts/rules/rule-value.ts +7 -1
- package/scripts/rules/shell-segments.ts +15 -0
- package/scripts/rules/stop-decision.ts +77 -0
- package/scripts/rules/stop-rules.ts +147 -47
- package/scripts/rules/tail-commands.ts +15 -1
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-closed.ts +8 -2
- package/scripts/run/board/run-board-github.ts +9 -1
- package/scripts/run/board/run-board-header.ts +4 -1
- package/scripts/run/board/run-board-labels.ts +2 -0
- package/scripts/run/board/run-board-layout.ts +1 -1
- package/scripts/run/board/run-board-read.ts +16 -0
- package/scripts/run/board/run-board-reason.ts +10 -1
- package/scripts/run/board/run-board-render-legend.ts +1 -0
- package/scripts/run/board/run-board-render-slot.ts +1 -1
- package/scripts/run/board/run-board-render-waits.ts +41 -0
- package/scripts/run/board/run-board-render.ts +6 -1
- package/scripts/run/board/run-board-row-yield.ts +1 -0
- package/scripts/run/board/run-board-state.ts +5 -0
- package/scripts/run/board/run-board-status.ts +72 -21
- package/scripts/run/board/run-board-tick.ts +5 -1
- package/scripts/run/cut/run-cut-cli.ts +3 -2
- package/scripts/run/cut/run-cut-report.ts +16 -0
- package/scripts/run/detached-launch.ts +23 -4
- package/scripts/run/entry/run-entry-cli.ts +8 -1
- package/scripts/run/event/run-event-render.ts +1 -0
- package/scripts/run/event/run-event-scope.ts +19 -1
- package/scripts/run/event/run-event-stream-emit.ts +13 -6
- package/scripts/run/event/run-event-stream.ts +4 -0
- package/scripts/run/hold/run-hold-session.ts +46 -0
- package/scripts/run/hold/run-hold.ts +7 -1
- package/scripts/run/launch-priority.ts +32 -0
- package/scripts/run/merge/run-merge-cli.ts +16 -65
- package/scripts/run/merge/run-merge-signals.ts +99 -0
- package/scripts/run/merge/run-merge-steps.ts +11 -1
- package/scripts/run/merge/run-merge.ts +21 -9
- package/scripts/run/run-liveness.ts +1 -1
- package/scripts/run/run-poll-hook.ts +157 -0
- package/scripts/run/run-step-cli.ts +22 -15
- package/scripts/run/run-step-section.ts +85 -0
- package/scripts/run/ship/run-ship-cli.ts +5 -12
- package/scripts/run/ship/run-ship-detach.ts +9 -0
- package/scripts/run/ship/run-ship-next.ts +10 -2
- package/scripts/run/ship/run-ship-overlap.ts +195 -0
- package/scripts/run/ship/run-ship-pre-detach.ts +33 -15
- package/scripts/run/ship/run-ship-preflight.ts +28 -2
- package/scripts/run/ship/run-ship-return.ts +31 -7
- package/scripts/run/ship/run-ship-review-steps.ts +12 -10
- package/scripts/run/ship/run-ship-steps.ts +1 -1
- package/scripts/run/ship/run-ship-stop-context.ts +206 -0
- package/scripts/run/ship/run-ship-sync.ts +14 -6
- package/scripts/test/test-e2e-guard.ts +30 -5
- package/scripts/test/test-unit-guard.ts +16 -1
- package/scripts/test/unit-worker-share.ts +26 -4
- package/scripts/time-runtime/time-shell.ts +29 -4
- package/scripts/time-runtime/time-spans.ts +1 -0
- package/scripts/time-runtime/time-tool-call.ts +7 -0
- package/templates/gitignore +6 -2
- package/dist/hooks/chunk-K4LMBQC2.js +0 -68
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { cost_blocks } from '#scripts/cost-runtime/cost-blocks'
|
|
2
|
+
import { json_value } from '#scripts/lib/json-value'
|
|
3
|
+
import type { GuardedCall } from '#scripts/time-runtime/time-batch-guard'
|
|
4
|
+
import { time_shell } from '#scripts/time-runtime/time-shell'
|
|
1
5
|
import { bash_triggers } from './bash-triggers'
|
|
2
6
|
import { gh_api } from './gh-api'
|
|
3
7
|
import { prior_comment_read } from './prior-comment-read'
|
|
@@ -77,6 +81,9 @@ function reads_issue_comments(command: string): boolean {
|
|
|
77
81
|
return shell_segments.segments_of(command).some((segment) => fetches_issue_comments(segment))
|
|
78
82
|
}
|
|
79
83
|
|
|
84
|
+
const RULE_DOC = '`.claude/skills/workflow-commands/issue-comments.md`'
|
|
85
|
+
const COMMENTS_FALLBACK = 'gh api repos/{owner}/{repo}/issues/<N>/comments'
|
|
86
|
+
|
|
80
87
|
// **The refusal hands over the command that fixes it**, because reading is not the same as
|
|
81
88
|
// obeying: a sentence saying "also read the comments" is prose that moves nothing, while a refused
|
|
82
89
|
// body read leaves the run holding the reissue that makes the comments *present*. The conflict rule
|
|
@@ -88,12 +95,62 @@ function reads_issue_comments(command: string): boolean {
|
|
|
88
95
|
// being the Issue the refused call named — so a run that reads only the opening still leaves holding
|
|
89
96
|
// the fix. The label keeps `time_transcript_line.guard_from_refusal` naming this guard, not the command.
|
|
90
97
|
const ISSUE_COMMENTS_REASON =
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
`⛔ issue comments: pnpm josh issue:read <N>\n` +
|
|
99
|
+
`Run that in place of this read — an Issue's comments are part of it, and a later comment ` +
|
|
100
|
+
`supersedes the body it contradicts; refused until that command is on the transcript. Without ` +
|
|
101
|
+
`josh, read \`${COMMENTS_FALLBACK}\` beside the body (${RULE_DOC}).`
|
|
102
|
+
|
|
103
|
+
// **A `gh api` body read is rewritten rather than refused** (joshuafolkken/kit#3920): the fix is the
|
|
104
|
+
// same Issue's comments endpoint, read on the same line, and the path the call already names is all
|
|
105
|
+
// it needs. Prepended on lines of their own, so a trailing comment or a `&&` chain on the typed line
|
|
106
|
+
// cannot swallow or skip them, and the typed line stays last — its exit status is still the call's.
|
|
107
|
+
// A `gh issue view` read is left to the refusal — its global flags (`--repo`) and its `--json`
|
|
108
|
+
// projection have no one comments spelling to add.
|
|
109
|
+
const COMMENTS_JQ = String.raw`'.[] | "— \(.user.login) \(.created_at)\n\(.body)\n"'`
|
|
110
|
+
|
|
111
|
+
const COMMENTS_NOTE =
|
|
112
|
+
"↻ issue comments: this read returned only the Issue's body, so the Issue's comments were fetched " +
|
|
113
|
+
`on the same line — a later comment supersedes the body it contradicts (${RULE_DOC}).`
|
|
114
|
+
|
|
115
|
+
// **Only a line that opens with the body read is rewritten.** Prepended ahead of a `cd` or a
|
|
116
|
+
// subshell, the comments read would resolve `{owner}/{repo}` in another directory and print another
|
|
117
|
+
// repository's comments; and the line is cut on its quote-blanked reading, so a body-read spelling
|
|
118
|
+
// quoted in a string or a heredoc body is never prepended as a real call. Every other line keeps the
|
|
119
|
+
// refusal.
|
|
120
|
+
function rewritable_path(command: string): string | undefined {
|
|
121
|
+
const read = shell_segments.first_of(command)
|
|
122
|
+
|
|
123
|
+
if (read === undefined || ISSUE_VIEW_COMMAND.test(read)) return undefined
|
|
124
|
+
|
|
125
|
+
return is_body_read_segment(read) ? ISSUE_BODY_PATH.exec(read)?.[0] : undefined
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function comments_command(command: string): string | undefined {
|
|
129
|
+
if (!is_body_only_issue_read(command)) return undefined
|
|
130
|
+
|
|
131
|
+
const path = rewritable_path(command)
|
|
132
|
+
|
|
133
|
+
if (path === undefined) return undefined
|
|
134
|
+
|
|
135
|
+
return `gh api --paginate ${path}/comments --jq ${COMMENTS_JQ}\n${command}`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// The call's input with the comments read prepended, or `undefined` where the comments are already on
|
|
139
|
+
// the tail or the read has no rewritable spelling.
|
|
140
|
+
function comments_input(
|
|
141
|
+
call: GuardedCall,
|
|
142
|
+
tail: () => string,
|
|
143
|
+
): Record<string, unknown> | undefined {
|
|
144
|
+
if (call.name !== cost_blocks.BASH_TOOL || !json_value.is_record(call.input)) return undefined
|
|
145
|
+
|
|
146
|
+
const command = comments_command(time_shell.bash_command(call.input))
|
|
147
|
+
|
|
148
|
+
if (command === undefined || prior_comment_read.already_read_for_call(tail(), call)) {
|
|
149
|
+
return undefined
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { ...call.input, command }
|
|
153
|
+
}
|
|
97
154
|
|
|
98
155
|
// The enumeration row itself, so `delivered-rules.ts` spreads one entry. `already_satisfied` stands it
|
|
99
156
|
// down once the comments are on the transcript tail, and until then it refuses every body-only read.
|
|
@@ -103,13 +160,17 @@ const ROW = {
|
|
|
103
160
|
reason: ISSUE_COMMENTS_REASON,
|
|
104
161
|
already_satisfied: prior_comment_read.already_read_for_call,
|
|
105
162
|
keeps: bash_triggers.on_bash_command(reads_issue_comments),
|
|
163
|
+
rewrite: comments_input,
|
|
164
|
+
rewrite_note: COMMENTS_NOTE,
|
|
106
165
|
// The opening the reason carried before it led with the command.
|
|
107
166
|
former_reasons: ["⛔ an Issue's comments are part of the Issue: read them before implementing"],
|
|
108
167
|
}
|
|
109
168
|
|
|
110
169
|
const issue_comments = {
|
|
170
|
+
COMMENTS_NOTE,
|
|
111
171
|
ISSUE_COMMENTS_REASON,
|
|
112
172
|
ROW,
|
|
173
|
+
comments_input,
|
|
113
174
|
is_body_only_issue_read,
|
|
114
175
|
}
|
|
115
176
|
|
|
@@ -30,10 +30,13 @@ const KEYWORD_BEFORE_ARGUMENT = -2
|
|
|
30
30
|
|
|
31
31
|
const PROMPT_LINE_SCHEMA = z.object({
|
|
32
32
|
isMeta: z.boolean().nullish(),
|
|
33
|
+
timestamp: z.string().nullish(),
|
|
33
34
|
origin: z.object({ kind: z.string().nullish() }).nullish(),
|
|
34
35
|
message: z.object({ content: z.unknown() }),
|
|
35
36
|
})
|
|
36
37
|
|
|
38
|
+
const TIMED_LINE_SCHEMA = z.object({ timestamp: z.string() })
|
|
39
|
+
|
|
37
40
|
type PromptLine = z.infer<typeof PROMPT_LINE_SCHEMA>
|
|
38
41
|
|
|
39
42
|
function is_person(line: PromptLine): boolean {
|
|
@@ -42,13 +45,20 @@ function is_person(line: PromptLine): boolean {
|
|
|
42
45
|
return line.origin === null || line.origin === undefined || line.origin.kind === HUMAN_ORIGIN
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
// The prompt line
|
|
46
|
-
|
|
48
|
+
// The prompt line as the person wrote it, or `undefined` when the harness wrote it or it does not
|
|
49
|
+
// read as one.
|
|
50
|
+
function person_line(line: string): PromptLine | undefined {
|
|
47
51
|
const parsed = PROMPT_LINE_SCHEMA.safeParse(json_value.parse_or_undefined(line))
|
|
48
52
|
|
|
49
|
-
|
|
53
|
+
return parsed.success && is_person(parsed.data) ? parsed.data : undefined
|
|
54
|
+
}
|
|
50
55
|
|
|
51
|
-
|
|
56
|
+
// The prompt line's text, or `undefined` when the harness wrote it or it does not read as one.
|
|
57
|
+
function person_text(line: string): string | undefined {
|
|
58
|
+
const person = person_line(line)
|
|
59
|
+
if (person === undefined) return undefined
|
|
60
|
+
|
|
61
|
+
return time_reported_failure.result_text(person.message.content)
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
// A keyword opening the prompt is what a person types; a keyword before its one trailing argument is a
|
|
@@ -75,6 +85,41 @@ function prompt_text(tail: string): string {
|
|
|
75
85
|
return text === undefined || is_invocation(text) ? NO_PROMPT : text
|
|
76
86
|
}
|
|
77
87
|
|
|
78
|
-
|
|
88
|
+
// **The prompt that opened this turn, when a person typed it** — '' when the harness opened it (a task
|
|
89
|
+
// notification, a subagent's hand-back), so a question asked turns ago does not answer for a stop the
|
|
90
|
+
// run reached on its own since (`stop-decision.ts` → `is_question`).
|
|
91
|
+
function turn_prompt(tail: string): string {
|
|
92
|
+
const opener = tail.split('\n').findLast((line) => filing_cap.is_prompt_line(line))
|
|
93
|
+
if (opener === undefined) return NO_PROMPT
|
|
94
|
+
|
|
95
|
+
return person_text(opener) ?? NO_PROMPT
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// When the person last prompted on the tail, or `undefined` when no prompt of theirs is on it.
|
|
99
|
+
function prompt_time(tail: string): string | undefined {
|
|
100
|
+
return tail
|
|
101
|
+
.split('\n')
|
|
102
|
+
.filter((line) => filing_cap.is_prompt_line(line))
|
|
103
|
+
.map((line) => person_line(line)?.timestamp ?? undefined)
|
|
104
|
+
.findLast((time) => time !== undefined)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The first dated line on the tail, or `undefined` when none is.
|
|
108
|
+
function tail_time(tail: string): string | undefined {
|
|
109
|
+
return tail
|
|
110
|
+
.split('\n')
|
|
111
|
+
.map((line) => TIMED_LINE_SCHEMA.safeParse(json_value.parse_or_undefined(line)).data?.timestamp)
|
|
112
|
+
.find((time) => time !== undefined)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// **When the current pause began, as far as the tail shows** — what a recorded notify is compared
|
|
116
|
+
// against. A person's prompt ends the pause it reported, so the last one opens the next; with none on
|
|
117
|
+
// the tail, only a send dated inside the tail is provably after every prompt, and an older send is
|
|
118
|
+
// announced again (joshuafolkken/kit#3961).
|
|
119
|
+
function pause_start(tail: string): string | undefined {
|
|
120
|
+
return prompt_time(tail) ?? tail_time(tail)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const last_prompt = { pause_start, prompt_text, turn_prompt }
|
|
79
124
|
|
|
80
125
|
export { last_prompt }
|
|
@@ -12,6 +12,10 @@ import { oracle_firing } from './oracle-firing'
|
|
|
12
12
|
const ARGV_OFFSET = 2
|
|
13
13
|
const INDENT = ' '
|
|
14
14
|
const FIELD_WIDTH = 10
|
|
15
|
+
// The rule-prose guard sends a rule author here first, so the list closes on the placement order a new
|
|
16
|
+
// rule is weighed against before it becomes a guard or a document line.
|
|
17
|
+
const AUTOMATION_ORDER =
|
|
18
|
+
'automate first: a command > a hook that runs it > a refusal guard > a document — a lower rung states why the higher cannot hold (`principles.md` → "automate-first"; a guard\'s `why` is `scripts/rules/guard-remedy.ts`)'
|
|
15
19
|
|
|
16
20
|
function pad_field(label: string): string {
|
|
17
21
|
return label.padEnd(FIELD_WIDTH)
|
|
@@ -51,6 +55,8 @@ function print_list(): void {
|
|
|
51
55
|
for (const oracle of decision_oracle.DECISION_ORACLES) {
|
|
52
56
|
console.info(format_oracle(oracle))
|
|
53
57
|
}
|
|
58
|
+
|
|
59
|
+
console.info(AUTOMATION_ORDER)
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
function run(argv: ReadonlyArray<string>): number {
|
|
@@ -70,6 +76,7 @@ function main(argv: ReadonlyArray<string>): void {
|
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
const oracle_list_cli = {
|
|
79
|
+
AUTOMATION_ORDER,
|
|
73
80
|
print_list,
|
|
74
81
|
}
|
|
75
82
|
|
|
@@ -124,6 +124,8 @@ function keeps_verdict_intact(command: string): boolean {
|
|
|
124
124
|
// **Only a filter that reads its input to the end qualifies.** One that exits early — `head`, or a
|
|
125
125
|
// `grep` stopping at its first match — closes the pipe on a check still writing, and under `pipefail` the
|
|
126
126
|
// check's SIGPIPE turns a passing run into exit 141; `head` also cuts the verdict line josh prints last.
|
|
127
|
+
// `cut`, `sort`, `uniq` and `wc` read to the end as well, and a `grep … | cut -c1-250` narrowing was the
|
|
128
|
+
// commonest masking still refused after #3875 (joshuafolkken/kit#3920).
|
|
127
129
|
//
|
|
128
130
|
// **Each masked check is wrapped on its own, and the rest of the line runs as typed.** Most of the
|
|
129
131
|
// refusals left after #3570 were a check chained beside other work — `vitest …; pnpm josh lint:related
|
|
@@ -141,7 +143,7 @@ function keeps_verdict_intact(command: string): boolean {
|
|
|
141
143
|
const PIPEFAIL_OPEN = '(set -o pipefail; '
|
|
142
144
|
const PIPEFAIL_CLOSE = ')'
|
|
143
145
|
const COMMENT_START = /(?:^|\s)#/u
|
|
144
|
-
const FULL_READING_FILTER = /^(?:tail|grep)\b/u
|
|
146
|
+
const FULL_READING_FILTER = /^(?:tail|grep|cut|sort|uniq|wc)\b/u
|
|
145
147
|
const EARLY_EXIT_GREP =
|
|
146
148
|
/^grep\b.*\s(?:-[a-zA-Z]*[lLmq]|--(?:quiet|silent|max-count|files-with(?:out)?-match))/u
|
|
147
149
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { delivered_rules } from './delivered-rules'
|
|
4
|
+
import { guard_remedy } from './guard-remedy'
|
|
4
5
|
import { rule_registry, type RegisteredRule } from './rule-registry'
|
|
5
6
|
|
|
6
7
|
// `josh rule:list` — the list of trigger-delivered rules, rendered from the guard rows themselves.
|
|
@@ -12,6 +13,7 @@ import { rule_registry, type RegisteredRule } from './rule-registry'
|
|
|
12
13
|
const HEADER =
|
|
13
14
|
'# Trigger-delivered rules — rendered by `pnpm josh rule:list` from `scripts/rules/delivered-rules.ts`'
|
|
14
15
|
const MISSING = '(no entry in `scripts/rules/rule-registry.ts`)'
|
|
16
|
+
const MISSING_WHY = '(no `why` in `scripts/rules/guard-remedy.ts`)'
|
|
15
17
|
|
|
16
18
|
function item(id: string, rule: RegisteredRule | undefined): string {
|
|
17
19
|
if (rule === undefined) return `- \`${id}\` ${MISSING}`
|
|
@@ -20,6 +22,7 @@ function item(id: string, rule: RegisteredRule | undefined): string {
|
|
|
20
22
|
`- **${rule.title}** (\`${id}\`, ${rule.topic})`,
|
|
21
23
|
` - fires: \`${rule.entry}\` — ${rule.fires}`,
|
|
22
24
|
` - silent: ${rule.quiet}`,
|
|
25
|
+
` - not automated: ${guard_remedy.why_of(id) ?? MISSING_WHY}`,
|
|
23
26
|
].join('\n')
|
|
24
27
|
}
|
|
25
28
|
|
|
@@ -46,6 +49,6 @@ function orphans(): ReadonlyArray<string> {
|
|
|
46
49
|
|
|
47
50
|
if (process.argv[1] === fileURLToPath(import.meta.url)) process.stdout.write(`${render()}\n`)
|
|
48
51
|
|
|
49
|
-
const rule_list = { MISSING, orphans, render }
|
|
52
|
+
const rule_list = { MISSING, MISSING_WHY, orphans, render }
|
|
50
53
|
|
|
51
54
|
export { rule_list }
|
|
@@ -157,6 +157,16 @@ const GUARD_ROWS: ReadonlyArray<RegisteredRule> = [
|
|
|
157
157
|
"a dispatched lane child's `AskUserQuestion`; it hands over the park procedure. Every occurrence",
|
|
158
158
|
quiet: 'not a lane child, or not an interactive tool',
|
|
159
159
|
},
|
|
160
|
+
{
|
|
161
|
+
id: 'agent-model',
|
|
162
|
+
title: 'Subagent model named',
|
|
163
|
+
topic:
|
|
164
|
+
'`.claude/skills/workflow-commands/delegation.md` → "A `general-purpose` subagent names its model"',
|
|
165
|
+
entry: RULE_GUARD,
|
|
166
|
+
fires:
|
|
167
|
+
'an `Agent` / `Task` call of the `general-purpose` type with no `model`. Every occurrence',
|
|
168
|
+
quiet: 'a named agent type, or a `model` set',
|
|
169
|
+
},
|
|
160
170
|
{
|
|
161
171
|
id: 'lane-carry-conflict',
|
|
162
172
|
title: 'Lane child carry commands',
|
|
@@ -45,14 +45,16 @@ const MEASURED_ROWS: ReadonlyArray<RegisteredRule> = [
|
|
|
45
45
|
},
|
|
46
46
|
]
|
|
47
47
|
|
|
48
|
-
// The `Stop` hook's rows; `stop:guard` judges every stop
|
|
48
|
+
// The `Stop` hook's rows; `stop:guard` judges every stop. A row with a fixed answer runs it itself and
|
|
49
|
+
// blocks only when that fails; a row that needs judgement blocks the stop while it holds.
|
|
49
50
|
const STOP_ROWS: ReadonlyArray<RegisteredRule> = [
|
|
50
51
|
{
|
|
51
52
|
id: 'stop-notify',
|
|
52
53
|
title: 'Stop notification',
|
|
53
54
|
topic: '`CLAUDE.md` → "Mid-workflow stop notification"',
|
|
54
55
|
entry: STOP_GUARD,
|
|
55
|
-
fires:
|
|
56
|
+
fires:
|
|
57
|
+
'a stop with the work tree held and no `confirmation` notify that turn. Sends the notify itself; blocks only when the send fails',
|
|
56
58
|
quiet: 'no hold, a `confirmation` notify on the tail, or a pre-gate cut taken',
|
|
57
59
|
},
|
|
58
60
|
{
|
|
@@ -60,7 +62,8 @@ const STOP_ROWS: ReadonlyArray<RegisteredRule> = [
|
|
|
60
62
|
title: 'Hold release',
|
|
61
63
|
topic: '`.claude/skills/workflow-commands/working-tree-hold.md`',
|
|
62
64
|
entry: STOP_GUARD,
|
|
63
|
-
fires:
|
|
65
|
+
fires:
|
|
66
|
+
'a stop on a clean tree with the `run:hold` record still in place. Releases the hold itself; blocks only when that fails',
|
|
64
67
|
quiet:
|
|
65
68
|
'a dirty tree (a `halfrun` stop before the commit, a `needs-human-review` stop) or already released',
|
|
66
69
|
},
|
|
@@ -70,7 +73,7 @@ const STOP_ROWS: ReadonlyArray<RegisteredRule> = [
|
|
|
70
73
|
topic: '`prompts/collaboration-workflow/issue-citation.md`',
|
|
71
74
|
entry: STOP_GUARD,
|
|
72
75
|
fires:
|
|
73
|
-
'a bare `#N` in the prose of the last reply.
|
|
76
|
+
'a bare `#N` in the prose of the last reply. Prints the citation lines itself; blocks only when a number cannot be cited',
|
|
74
77
|
quiet: 'link form, inside code or a quote, `#N` right after `PR`, or a GitHub-facing artifact',
|
|
75
78
|
},
|
|
76
79
|
{
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { hook_decision } from '#scripts/josh/hook-decision'
|
|
2
|
+
import { json_value } from '#scripts/lib/json-value'
|
|
3
|
+
import { time_density_hook } from '#scripts/time-runtime/time-density-hook'
|
|
4
|
+
import { time_hook_transcript } from '#scripts/time-runtime/time-hook-transcript'
|
|
5
|
+
import { delivered_rules, type DeliveredRule } from './delivered-rules'
|
|
6
|
+
import { guard_remedy } from './guard-remedy'
|
|
7
|
+
|
|
8
|
+
// The call a `PreToolUse` hook runs in place of a mechanically fixable refusal (joshuafolkken/kit#3920).
|
|
9
|
+
// Each row `guard-remedy.ts` classes as a rewrite gets the call in turn, so one line that both pipes a
|
|
10
|
+
// check and reads a body is made right by both rows. The rewrite is the rule guard's delivery, so the
|
|
11
|
+
// rule guard's switch turns it off too.
|
|
12
|
+
|
|
13
|
+
interface RuleRewrite {
|
|
14
|
+
// The payload carrying the rewritten input, which the guards judge in place of the typed one.
|
|
15
|
+
payload: string
|
|
16
|
+
input: Record<string, unknown>
|
|
17
|
+
// The notes of the rows that rewrote, in row order.
|
|
18
|
+
note: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface Folded {
|
|
22
|
+
input: Record<string, unknown>
|
|
23
|
+
notes: ReadonlyArray<string>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const REWRITING_RULES: ReadonlyArray<DeliveredRule> = delivered_rules.DELIVERED_RULES.filter(
|
|
27
|
+
(rule) => guard_remedy.is_rewrite(rule.id) && rule.rewrite !== undefined,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
const NOTE_SEPARATOR = '\n\n'
|
|
31
|
+
|
|
32
|
+
// **A tail that cannot be read is an empty one**, as every guard fails open (`hook-decision.ts`): a
|
|
33
|
+
// throw here would end the hook before any guard judged the call.
|
|
34
|
+
function read_payload_tail(raw_payload: string): string {
|
|
35
|
+
try {
|
|
36
|
+
const payload = hook_decision.parse_hook_payload(raw_payload)
|
|
37
|
+
|
|
38
|
+
return payload
|
|
39
|
+
? time_density_hook.read_tail(
|
|
40
|
+
time_hook_transcript.transcript_of(payload.transcript_path, payload.agent_id),
|
|
41
|
+
)
|
|
42
|
+
: ''
|
|
43
|
+
} catch {
|
|
44
|
+
return ''
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function tail_reader(raw_payload: string): () => string {
|
|
49
|
+
const cache: { tail?: string } = {}
|
|
50
|
+
|
|
51
|
+
return function tail(): string {
|
|
52
|
+
cache.tail ??= read_payload_tail(raw_payload)
|
|
53
|
+
|
|
54
|
+
return cache.tail
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function fold(name: string, typed: Record<string, unknown>, tail: () => string): Folded {
|
|
59
|
+
const folded: Folded = { input: typed, notes: [] }
|
|
60
|
+
|
|
61
|
+
for (const rule of REWRITING_RULES) {
|
|
62
|
+
const input = rule.rewrite?.({ name, input: folded.input }, tail)
|
|
63
|
+
|
|
64
|
+
if (input === undefined) {
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
folded.input = input
|
|
69
|
+
folded.notes = [...folded.notes, rule.rewrite_note ?? '']
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return folded
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// The typed call, beside the whole payload its rewrite is spread back into.
|
|
76
|
+
interface TypedPayload {
|
|
77
|
+
fields: Record<string, unknown>
|
|
78
|
+
name: string
|
|
79
|
+
input: Record<string, unknown>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function typed_payload(raw_payload: string): TypedPayload | undefined {
|
|
83
|
+
const fields = json_value.parse_or_undefined(raw_payload)
|
|
84
|
+
|
|
85
|
+
if (!json_value.is_record(fields)) return undefined
|
|
86
|
+
|
|
87
|
+
const input = fields['tool_input']
|
|
88
|
+
|
|
89
|
+
return json_value.is_record(input)
|
|
90
|
+
? { fields, name: String(fields['tool_name']), input }
|
|
91
|
+
: undefined
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function rewrite(raw_payload: string): RuleRewrite | undefined {
|
|
95
|
+
const typed = delivered_rules.is_enabled() ? typed_payload(raw_payload) : undefined
|
|
96
|
+
|
|
97
|
+
if (typed === undefined) return undefined
|
|
98
|
+
|
|
99
|
+
const { input, notes } = fold(typed.name, typed.input, tail_reader(raw_payload))
|
|
100
|
+
|
|
101
|
+
if (notes.length === 0) return undefined
|
|
102
|
+
|
|
103
|
+
const note = notes.join(NOTE_SEPARATOR)
|
|
104
|
+
|
|
105
|
+
return { payload: JSON.stringify({ ...typed.fields, tool_input: input }), input, note }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const rule_rewrite = {
|
|
109
|
+
NOTE_SEPARATOR,
|
|
110
|
+
rewrite,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type { RuleRewrite }
|
|
114
|
+
export { rule_rewrite }
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from '#scripts/time-runtime/time-transcript-line'
|
|
6
6
|
import { delivered_rules, type MeasuredRule } from './delivered-rules'
|
|
7
7
|
import { hook_context_line } from './hook-context-line'
|
|
8
|
+
import { rule_rewrite } from './rule-rewrite'
|
|
8
9
|
|
|
9
10
|
// What a rule is worth on the channel that carries it when its delivery has not fired.
|
|
10
11
|
//
|
|
@@ -287,7 +288,7 @@ function rewritten_id(text: string): string | undefined {
|
|
|
287
288
|
return found?.id
|
|
288
289
|
}
|
|
289
290
|
|
|
290
|
-
function
|
|
291
|
+
function observe_note(states: Map<string, RuleState>, text: string): void {
|
|
291
292
|
const id = rewritten_id(text)
|
|
292
293
|
const state = id === undefined ? undefined : states.get(id)
|
|
293
294
|
|
|
@@ -297,6 +298,11 @@ function observe_rewrite(states: Map<string, RuleState>, text: string): void {
|
|
|
297
298
|
state.is_triggered = true
|
|
298
299
|
}
|
|
299
300
|
|
|
301
|
+
// One line can be rewritten by several rows, whose notes `rule-rewrite.ts` joins into one context.
|
|
302
|
+
function observe_rewrite(states: Map<string, RuleState>, text: string): void {
|
|
303
|
+
for (const note of text.split(rule_rewrite.NOTE_SEPARATOR)) observe_note(states, note)
|
|
304
|
+
}
|
|
305
|
+
|
|
300
306
|
function observe_for_rule(
|
|
301
307
|
rule: MeasuredRule,
|
|
302
308
|
state: RuleState,
|
|
@@ -26,6 +26,20 @@ function parts_of(command: string): Array<string> {
|
|
|
26
26
|
return command.split(SEGMENT_PARTS)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* The line's first command as the shell runs it — cut where the quote-blanked line puts the first
|
|
31
|
+
* separator, so one inside a quoted string does not cut it — or `undefined` for a heredoc line.
|
|
32
|
+
*/
|
|
33
|
+
function first_of(command: string): string | undefined {
|
|
34
|
+
const plain = time_shell.blanked(command)
|
|
35
|
+
|
|
36
|
+
if (plain === undefined) return undefined
|
|
37
|
+
|
|
38
|
+
const cut = plain.search(SEGMENT_SEPARATOR)
|
|
39
|
+
|
|
40
|
+
return (cut === -1 ? command : command.slice(0, cut)).trim()
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
// **Both spellings match, and the expansion is not this file's.** `josh_command_of` expands the alias
|
|
30
44
|
// in the command it reads, so `pnpm josh ga` arrives as `josh gate` and a caller's canonical set
|
|
31
45
|
// matches it as it is. Widening as well would be a second copy of one rule, and the caller that
|
|
@@ -41,6 +55,7 @@ function is_josh_command(segment: string, names: ReadonlySet<string>): boolean {
|
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
const shell_segments = {
|
|
58
|
+
first_of,
|
|
44
59
|
is_josh_command,
|
|
45
60
|
parts_of,
|
|
46
61
|
segments_of,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// What a held stop is waiting on, read from records rather than guessed from prose
|
|
2
|
+
// (joshuafolkken/kit#3961): the decision lines the reply states, and whether the person's own prompt
|
|
3
|
+
// this turn was a question.
|
|
4
|
+
//
|
|
5
|
+
// **The recommended choice is the reply's, never a fixed placeholder.** A `confirmation` reaches a
|
|
6
|
+
// person away from the session, and "read the reply" tells them nothing about what they are asked to
|
|
7
|
+
// decide. The reply names it on a line of its own — `Recommended: <choice>`, with an optional
|
|
8
|
+
// `Alternative: <choice>` — the same two fields `josh notify` takes, so the hook copies them rather
|
|
9
|
+
// than inferring them.
|
|
10
|
+
//
|
|
11
|
+
// **A turn the person opened with a question is answered in the session they are at.** Its stop is
|
|
12
|
+
// the answer, not a pause, so a Telegram saying "confirmation required" only buries the ones that are.
|
|
13
|
+
|
|
14
|
+
interface StopDecision {
|
|
15
|
+
recommended: string
|
|
16
|
+
alternative: string | undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// A label may close its emphasis before or after the colon: `**Recommended:**`, `**Recommended**:`.
|
|
20
|
+
const RECOMMENDED_LABEL = /^recommended[*_]*:/iu
|
|
21
|
+
const ALTERNATIVE_LABEL = /^alternative[*_]*:/iu
|
|
22
|
+
// The list bullet and the Markdown emphasis a decision line may be written with. Emphasis is
|
|
23
|
+
// stripped only at the edges of the label and the choice: an underscore inside a choice belongs to
|
|
24
|
+
// a `snake_case` name the person is asked to approve.
|
|
25
|
+
const LEADING_EMPHASIS = /^[*_]+/u
|
|
26
|
+
const TRAILING_EMPHASIS = /(?<![*_])[*_]+$/u
|
|
27
|
+
const BULLET = /^[-*]\s+/u
|
|
28
|
+
|
|
29
|
+
// A prompt that ends asking: a question mark, or a Japanese sentence-final question particle, before
|
|
30
|
+
// any trailing full stop or space.
|
|
31
|
+
const QUESTION_ENDING = /(?:[??]|か|かな|かね)[\s。.]*$/u
|
|
32
|
+
|
|
33
|
+
function unwrapped(text: string): string {
|
|
34
|
+
return text.trim().replace(LEADING_EMPHASIS, '').replace(TRAILING_EMPHASIS, '').trim()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function bare_line(line: string): string {
|
|
38
|
+
return unwrapped(line.trim().replace(BULLET, ''))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The choice on the reply's first line labelled `label`, or `undefined` when none carries one.
|
|
42
|
+
function choice_of(message: string, label: RegExp): string | undefined {
|
|
43
|
+
const line = message
|
|
44
|
+
.split('\n')
|
|
45
|
+
.map((one) => bare_line(one))
|
|
46
|
+
.find((one) => label.test(one))
|
|
47
|
+
const choice = line === undefined ? undefined : unwrapped(line.replace(label, ''))
|
|
48
|
+
|
|
49
|
+
return choice === '' ? undefined : choice
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The decision the reply asks for, or `undefined` when it names no recommended choice.
|
|
53
|
+
function decision_of(message: string): StopDecision | undefined {
|
|
54
|
+
const recommended = choice_of(message, RECOMMENDED_LABEL)
|
|
55
|
+
if (recommended === undefined) return undefined
|
|
56
|
+
|
|
57
|
+
return { recommended, alternative: choice_of(message, ALTERNATIVE_LABEL) }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function is_question(prompt: string): boolean {
|
|
61
|
+
return QUESTION_ENDING.test(prompt.trim())
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// **A held stop with no decision line is sent back once rather than announced.** Either it waits on a
|
|
65
|
+
// person and has to say on what, or it waits on nobody and the notify would be the noise this rule
|
|
66
|
+
// exists to end; only the run can tell which, so the reason hands it both answers.
|
|
67
|
+
const DECISION_REASON =
|
|
68
|
+
'⛔ stop decision: this working tree is held by your run and your reply names no decision. If the ' +
|
|
69
|
+
'run waits on a person, end the reply with a `Recommended: <choice>` line and, where there is ' +
|
|
70
|
+
'one, an `Alternative: <choice>` line — the hook sends the `confirmation` Telegram from them. If ' +
|
|
71
|
+
'it waits on nobody, continue the run, or say in one line that nothing is awaited. Do not repeat ' +
|
|
72
|
+
'your previous reply.'
|
|
73
|
+
|
|
74
|
+
const stop_decision = { DECISION_REASON, decision_of, is_question }
|
|
75
|
+
|
|
76
|
+
export type { StopDecision }
|
|
77
|
+
export { stop_decision }
|