@joshuafolkken/kit 1.1128.0 → 1.1166.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/investigator.md +17 -0
- package/.claude/settings.json +5 -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 +11 -60
- package/.claude/skills/workflow-commands/backlogrun-park.md +10 -8
- package/.claude/skills/workflow-commands/backlogrun-progress.md +19 -175
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +155 -0
- package/.claude/skills/workflow-commands/backlogrun-steps.md +64 -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 +7 -3
- 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 +33 -93
- 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/AGENTS.md +1 -6
- package/CLAUDE.md +58 -120
- package/GEMINI.md +1 -6
- package/SECURITY.md +1 -0
- package/dist/CLAUDE.md +58 -120
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-MKVGBQPA.js → chunk-5KTCMCOT.js} +14 -1
- package/dist/hooks/{chunk-NXYMNPUQ.js → chunk-A6P7PN7P.js} +59 -21
- package/dist/hooks/{chunk-77J7IEQM.js → chunk-ANJ2WGFD.js} +1 -1
- package/dist/hooks/{chunk-4JZDP5X5.js → chunk-DYAVAM6S.js} +572 -127
- package/dist/hooks/{chunk-7HVB7SZG.js → chunk-ENA7P5FB.js} +4 -4
- package/dist/hooks/{chunk-M7QKEKWO.js → chunk-GU5MATVE.js} +214 -85
- package/dist/hooks/chunk-GYNGP7HI.js +56 -0
- package/dist/hooks/{chunk-5SD2YM7Z.js → chunk-URJA6XY4.js} +4 -4
- package/dist/hooks/{chunk-XYUIQOW3.js → chunk-V2BTOZHT.js} +6 -4
- package/dist/hooks/{chunk-AIOIOMPX.js → chunk-ZDZ555YH.js} +225 -78
- 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 +186 -184
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +11 -10
- package/dist/hooks/stop-guard.js +5 -5
- package/dist/josh.js +33 -3
- package/dist/version/index.js +5 -1
- package/package.json +7 -4
- 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-offer.ts +6 -6
- package/scripts/behavior/behavior-rules.ts +11 -4
- package/scripts/delegation/delegation-policy.ts +9 -1
- package/scripts/delegation/investigation-guard.ts +1 -1
- package/scripts/delegation/investigation-reads.ts +109 -23
- 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/capped-print.ts +74 -0
- package/scripts/document/document-byte-budget.ts +34 -20
- package/scripts/document/document-section-cli.ts +4 -3
- 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/gate/scoped-green.ts +25 -14
- package/scripts/gh/git-gh-api-path.ts +7 -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/claude-kit-only-settings.ts +29 -0
- package/scripts/init/hook-command-rewrite.ts +18 -35
- package/scripts/init/hook-launch.ts +21 -32
- package/scripts/init/init-copy-content.ts +8 -2
- package/scripts/init/initial-commit.ts +24 -0
- package/scripts/init/start-exec.ts +16 -3
- package/scripts/init/start-steps.ts +8 -12
- 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 +4 -3
- package/scripts/issue/issue-read.ts +2 -2
- package/scripts/issue/issue-report-failures.ts +6 -2
- package/scripts/issue/issue-scout-cli.ts +2 -2
- package/scripts/issue/issue-state-cli.ts +2 -2
- 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 +10 -0
- package/scripts/josh/josh-commands-issue.ts +2 -2
- package/scripts/josh/josh-commands-lane.ts +1 -1
- package/scripts/josh/josh-commands-project.ts +7 -0
- package/scripts/lane/lane-await.ts +46 -2
- package/scripts/lane/lane-cli.ts +11 -13
- package/scripts/lines/bytes-command.ts +29 -6
- package/scripts/observations/observation-ledger-line.ts +1 -1
- package/scripts/release/github-release-environment.ts +1 -0
- package/scripts/release/github-release.ts +103 -17
- 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/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-cli.ts +3 -1
- package/scripts/run/run-carry-conversation.ts +94 -0
- package/scripts/run/run-carry-reclaim.ts +37 -0
- package/scripts/run/run-carry.ts +21 -5
- package/scripts/run/run-cut-handoff.ts +21 -1
- 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 +12 -4
- 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-preflight.ts +34 -3
- package/scripts/run/run-ship-review-steps.ts +1 -1
- package/scripts/run/run-ship-review.ts +26 -5
- package/scripts/run/run-step-cli.ts +20 -7
- package/scripts/run/run-wake-driver.ts +24 -1
- package/scripts/run/run-wake-session.ts +2 -1
- package/scripts/test/test-declared-logic.ts +1 -1
- package/scripts/test/test-related.ts +1 -6
- package/scripts/test/test-unit-guard.ts +21 -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-spans.ts +4 -0
- package/scripts/version/npm-registry.ts +11 -1
- package/templates/workflows/ci.yml +9 -1
- package/templates/workflows/github-release.yml +13 -25
- package/.claude/skills/workflow-commands/followup-reference.md +0 -118
- package/dist/hooks/chunk-QVLT52OV.js +0 -464
|
@@ -14,31 +14,67 @@ import { shell_segments } from './shell-segments'
|
|
|
14
14
|
// existing file, and a synchronous `stat` decides it — a new-file creation and a read-only heredoc stay
|
|
15
15
|
// silent, an existing-file rewrite is refused.
|
|
16
16
|
//
|
|
17
|
-
// **
|
|
17
|
+
// **Four shapes, one row.** (1) A heredoc whose redirect or `tee` target already exists. (2) an
|
|
18
18
|
// interpreter reading a heredoc — or a `node -e` — whose body writes a file: the canonical `python3 -
|
|
19
19
|
// <<'PY' … open(f,'w') … PY`, which has no shell redirect to stat, is caught by its body instead. (3)
|
|
20
|
-
// an in-place `perl -0pi -e`.
|
|
21
|
-
//
|
|
20
|
+
// an in-place `perl -0pi -e`. (4) a heredoc creating an interpreter script whose body writes. Each is
|
|
21
|
+
// read on every command line, not only the first. A read-only heredoc (`cat <<'EOF'`, a compute-only
|
|
22
|
+
// `python3 -`) writes nothing and stays silent — the enumeration's rule that a non-firing turn is a
|
|
23
|
+
// kept turn.
|
|
22
24
|
//
|
|
23
25
|
// **It fires on every occurrence** (`git-force.ts`): each inline body is paid for again, so
|
|
24
26
|
// refused-once-and-free-after would let the second rewrite through.
|
|
25
27
|
|
|
26
|
-
// A heredoc marker in any of its spellings: `<<EOF`, `<< 'EOF'`, `<<-"EOF"
|
|
27
|
-
// separates a body-carrying write from an ordinary redirect a run legitimately uses
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
// and the write indicators are scanned in the body.
|
|
28
|
+
// A heredoc marker in any of its spellings: `<<EOF`, `<< 'EOF'`, `<<-"EOF"`, with its delimiter captured.
|
|
29
|
+
// Its presence is what separates a body-carrying write from an ordinary redirect a run legitimately uses
|
|
30
|
+
// to extract text. A here-string (`<<<`) and an arithmetic shift (`$((1<<2))`) carry no body, so a third
|
|
31
|
+
// `<` on either side and a digit delimiter are excluded.
|
|
32
|
+
const HEREDOC = /(?<!<)<<(?!<)-?\s*['"]?([A-Z_a-z]\w*)/u
|
|
32
33
|
const LINE_BREAK = '\n'
|
|
33
34
|
|
|
34
|
-
function
|
|
35
|
-
|
|
35
|
+
function has_heredoc(command: string): boolean {
|
|
36
|
+
return HEREDOC.test(command)
|
|
37
|
+
}
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
// The lines the shell reads as commands — every physical line outside a heredoc body — each paired with
|
|
40
|
+
// the heredoc body it opened. A command, its redirect and its heredoc marker share one of these; the body
|
|
41
|
+
// follows up to the delimiter line. Reading only the first physical line missed the interpreter a run
|
|
42
|
+
// opened after a `cd <lane>` line (joshuafolkken/kit#3155), so every command line is read, a body line is
|
|
43
|
+
// never mistaken for one, and a write is judged only in the body its own command opened.
|
|
44
|
+
interface CommandLine {
|
|
45
|
+
text: string
|
|
46
|
+
body: string
|
|
38
47
|
}
|
|
39
48
|
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
interface LineScan {
|
|
50
|
+
lines: ReadonlyArray<CommandLine>
|
|
51
|
+
delimiter: string | undefined
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function with_body_line(lines: ReadonlyArray<CommandLine>, line: string): Array<CommandLine> {
|
|
55
|
+
const opener = lines.at(-1)
|
|
56
|
+
|
|
57
|
+
if (opener === undefined) return [...lines]
|
|
58
|
+
|
|
59
|
+
return [...lines.slice(0, -1), { ...opener, body: `${opener.body}${line}${LINE_BREAK}` }]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function scan_line(scan: LineScan, line: string): LineScan {
|
|
63
|
+
if (scan.delimiter === undefined) {
|
|
64
|
+
return { lines: [...scan.lines, { text: line, body: '' }], delimiter: HEREDOC.exec(line)?.[1] }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (line.trim() === scan.delimiter) return { ...scan, delimiter: undefined }
|
|
68
|
+
|
|
69
|
+
return { ...scan, lines: with_body_line(scan.lines, line) }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function command_lines(command: string): ReadonlyArray<CommandLine> {
|
|
73
|
+
let scan: LineScan = { lines: [], delimiter: undefined }
|
|
74
|
+
|
|
75
|
+
for (const line of command.split(LINE_BREAK)) scan = scan_line(scan, line)
|
|
76
|
+
|
|
77
|
+
return scan.lines
|
|
42
78
|
}
|
|
43
79
|
|
|
44
80
|
// A file redirect (`> path`, `>> path`) or a `tee path`, with the target captured. A descriptor
|
|
@@ -53,40 +89,56 @@ function redirect_target(line: string): string | undefined {
|
|
|
53
89
|
return target === undefined ? undefined : target.replaceAll(QUOTES, '')
|
|
54
90
|
}
|
|
55
91
|
|
|
56
|
-
// Shape 1: a heredoc write whose target already exists. The existence check is injected so the suite
|
|
57
|
-
// controls it without touching the real filesystem, the same way `lane-park.ts` injects the lane fact.
|
|
58
|
-
function writes_existing_file(command: string, has_file: (path: string) => boolean): boolean {
|
|
59
|
-
if (!has_heredoc(command)) return false
|
|
60
|
-
|
|
61
|
-
const target = redirect_target(first_line(command))
|
|
62
|
-
|
|
63
|
-
return target !== undefined && has_file(target)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
92
|
// The interpreters that carry a body to run rather than a region to read. `cat` / `grep` / `sed -n`
|
|
67
93
|
// are deliberately absent — a heredoc fed to them is the read-only form `file-edits.md` allows.
|
|
68
|
-
const BODY_INTERPRETERS: ReadonlySet<string> = new Set([
|
|
94
|
+
const BODY_INTERPRETERS: ReadonlySet<string> = new Set([
|
|
95
|
+
'python',
|
|
96
|
+
'python3',
|
|
97
|
+
'ruby',
|
|
98
|
+
'perl',
|
|
99
|
+
'php',
|
|
100
|
+
'node',
|
|
101
|
+
])
|
|
69
102
|
const NODE = 'node'
|
|
70
103
|
const NODE_EVAL = /(?:^|\s)(?:-e|--eval)\b/u
|
|
71
104
|
|
|
72
105
|
// A body that writes a file, in Python and Node spellings — split across two patterns so neither
|
|
73
106
|
// exceeds the regex-complexity ceiling. The named write calls: `writeFile` covers `writeFileSync`,
|
|
74
|
-
// `appendFile` covers its sync form, and `.write(` covers `.writelines(
|
|
75
|
-
|
|
107
|
+
// `appendFile` covers its sync form, and `.write(` covers `.writelines(` and Python's path-object
|
|
108
|
+
// `.write_text(` / `.write_bytes(`.
|
|
109
|
+
const WRITE_CALL = /writeFile|appendFile|createWriteStream|\.write(?:lines|_text|_bytes)?\s*\(/u
|
|
76
110
|
// The mode-string and relocation forms: an `open(…, "w"|"a"|"x")`, `os.replace`, `shutil.copy` / `move`.
|
|
77
111
|
// A mode string of `r` alone (a read) matches none of these, so a compute- or read-only heredoc stays
|
|
78
112
|
// silent.
|
|
79
113
|
const WRITE_MODE = /open\s*\([^)]*['"][rbt+]*[wax]|os\.replace|shutil\.(?:copy|move)/u
|
|
80
114
|
|
|
81
|
-
function body_writes(
|
|
82
|
-
return WRITE_CALL.test(
|
|
115
|
+
function body_writes(body: string): boolean {
|
|
116
|
+
return WRITE_CALL.test(body) || WRITE_MODE.test(body)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// A script file for one of the body interpreters. Creating one is a new-file creation, but a script
|
|
120
|
+
// whose body writes is the interpreter rewrite moved one call later — `cat > edit.py <<EOF` then
|
|
121
|
+
// `python3 edit.py` carries the same replacement text (joshuafolkken/kit#3155).
|
|
122
|
+
const SCRIPT_FILE = /\.(?:py|rb|pl|php|js|mjs|cjs)$/u
|
|
123
|
+
|
|
124
|
+
// Shapes 1 and 4: a heredoc write whose target already exists, or whose target is a script that writes.
|
|
125
|
+
// The existence check is injected so the suite controls it without touching the real filesystem, the
|
|
126
|
+
// same way `lane-park.ts` injects the lane fact.
|
|
127
|
+
function is_heredoc_write(line: CommandLine, has_file: (path: string) => boolean): boolean {
|
|
128
|
+
const target = has_heredoc(line.text) ? redirect_target(line.text) : undefined
|
|
129
|
+
|
|
130
|
+
if (target === undefined) return false
|
|
131
|
+
if (has_file(target)) return true
|
|
132
|
+
|
|
133
|
+
return SCRIPT_FILE.test(target) && body_writes(line.body)
|
|
83
134
|
}
|
|
84
135
|
|
|
85
136
|
// Shape 2: an interpreter reading a heredoc, or a `node -e`, whose body writes.
|
|
86
|
-
function is_interpreter_write(segment: string,
|
|
137
|
+
function is_interpreter_write(segment: string, body: string): boolean {
|
|
87
138
|
const leading = time_shell.leading_word(segment)
|
|
88
139
|
|
|
89
|
-
if (BODY_INTERPRETERS.has(leading)) return
|
|
140
|
+
if (!BODY_INTERPRETERS.has(leading)) return false
|
|
141
|
+
if (has_heredoc(segment)) return body_writes(body)
|
|
90
142
|
|
|
91
143
|
return leading === NODE && NODE_EVAL.test(segment) && body_writes(segment)
|
|
92
144
|
}
|
|
@@ -103,19 +155,21 @@ function is_in_place_perl(segment: string): boolean {
|
|
|
103
155
|
return segment.split(WHITESPACE).some((word) => PERL_IN_PLACE.test(word))
|
|
104
156
|
}
|
|
105
157
|
|
|
106
|
-
// The interpreter and perl shapes are read per
|
|
107
|
-
//
|
|
108
|
-
function has_body_interpreter(
|
|
158
|
+
// The interpreter and perl shapes are read per command-line segment, so a heredoc marker is judged
|
|
159
|
+
// against the command that opened it rather than against a later one in a chain.
|
|
160
|
+
function has_body_interpreter(line: CommandLine): boolean {
|
|
109
161
|
return shell_segments
|
|
110
|
-
.segments_of(
|
|
111
|
-
.some((segment) => is_interpreter_write(segment,
|
|
162
|
+
.segments_of(line.text)
|
|
163
|
+
.some((segment) => is_interpreter_write(segment, line.body) || is_in_place_perl(segment))
|
|
112
164
|
}
|
|
113
165
|
|
|
114
166
|
function carries_a_file_body(
|
|
115
167
|
command: string,
|
|
116
168
|
has_file: (path: string) => boolean = existsSync,
|
|
117
169
|
): boolean {
|
|
118
|
-
return
|
|
170
|
+
return command_lines(command).some(
|
|
171
|
+
(line) => is_heredoc_write(line, has_file) || has_body_interpreter(line),
|
|
172
|
+
)
|
|
119
173
|
}
|
|
120
174
|
|
|
121
175
|
// The instruction in the shape a refusal can carry: what the command is doing, the safe tool, and the
|
|
@@ -123,11 +177,13 @@ function carries_a_file_body(
|
|
|
123
177
|
// surprising — the command body is billed on every later request of the run.
|
|
124
178
|
const FILE_BODY_REASON =
|
|
125
179
|
"⛔ file body in a shell command: this carries a file's new text inline — a heredoc rewriting an " +
|
|
126
|
-
'existing file, an interpreter (`python3 - <<`, `node -e`) whose body writes,
|
|
127
|
-
'-0pi -e`. `CLAUDE.md` forbids it because
|
|
128
|
-
'
|
|
129
|
-
'
|
|
130
|
-
'a
|
|
180
|
+
'existing file, an interpreter (`python3 - <<`, `node -e`) whose body writes, a script file (`cat > ' +
|
|
181
|
+
'edit.py <<EOF`) whose body writes, or an in-place `perl -0pi -e`. `CLAUDE.md` forbids it because ' +
|
|
182
|
+
'the command body is re-read on every later request of the run, so an editing script written early ' +
|
|
183
|
+
'is billed for the rest of it (`file-edits.md` measured one run at ~2m45s spent retyping existing ' +
|
|
184
|
+
'lines). Edit a region with the Edit tool (old / new); a change scattered so widely that the ' +
|
|
185
|
+
'partial edits would carry more than the file is rewritten whole with the Write tool, never a ' +
|
|
186
|
+
'heredoc. Pass a body by path (`--field body=@<path>`, `--body-file <path>`). A new-file creation (`cat > ' +
|
|
131
187
|
'<new-file> <<EOF`), a read-only heredoc (`cat <<EOF`), and a short `sed -i` stay allowed. **This ' +
|
|
132
188
|
'rule fires on every occurrence, not once per run.**'
|
|
133
189
|
|
|
@@ -8,7 +8,7 @@ import { tail_commands } from './tail-commands'
|
|
|
8
8
|
// The trigger and the decision behind the `filing-cap` row of `delivered-rules.ts`
|
|
9
9
|
// (joshuafolkken/kit#2119).
|
|
10
10
|
//
|
|
11
|
-
// **Automatic filing is capped at 10 Issues per run, refused at the eleventh filing.**
|
|
11
|
+
// **Automatic filing is capped at 10 Issues per run, refused at the eleventh filing.** `prerequisite.md` and
|
|
12
12
|
// `observation-filing.md` state the ceiling; nothing counted it, so a run that over-filed was held
|
|
13
13
|
// only by self-restraint. The input is mechanical — how many Issues this run has already filed — so
|
|
14
14
|
// it is a delivered rule rather than resident prose. **No new predicate**: the filing is
|
|
@@ -108,7 +108,7 @@ const FILING_CAP_REASON =
|
|
|
108
108
|
`⛔ filing cap: this run has already filed ${String(FILING_CAP)} Issues, the per-run ceiling — on ` +
|
|
109
109
|
'reaching it, stop and report rather than filing more. Automatic filing is capped so a run cannot ' +
|
|
110
110
|
'manufacture Issues faster than they can be closed (`.claude/skills/workflow-commands/SKILL.md` → ' +
|
|
111
|
-
"
|
|
111
|
+
"`prerequisite.md`, `observation-filing.md`). A further Issue that genuinely must exist is a person's call, not " +
|
|
112
112
|
"this run's. **This rule fires on every filing past the cap, not once per run**, so reissuing the " +
|
|
113
113
|
'same filing will be refused again.'
|
|
114
114
|
|
|
@@ -4,7 +4,7 @@ import { issue_citation } from './issue-citation'
|
|
|
4
4
|
// The predicates behind the `filing-offer` row of the stop guard (joshuafolkken/kit#2422).
|
|
5
5
|
//
|
|
6
6
|
// **A first-party filing is Tier A, and an offer to file is that filing deferred to the user.**
|
|
7
|
-
// `
|
|
7
|
+
// `observation-filing.md` says a run that judges something worth filing files it without asking, yet a reply
|
|
8
8
|
// can still end on "it is worth filing — say so if you want it filed". The rule was resident; what was
|
|
9
9
|
// missing was anything that fired at the moment of hesitation. That moment is one `Stop` payload: a
|
|
10
10
|
// reply that offers to file, on a turn that filed nothing.
|
package/scripts/rules/gh-api.ts
CHANGED
|
@@ -30,6 +30,11 @@ const REPO_PATH = /repos\/([^/\s'"?]+)\/([^/\s'"?]+)/u
|
|
|
30
30
|
const OWNER_GROUP = 1
|
|
31
31
|
const REPO_GROUP = 2
|
|
32
32
|
|
|
33
|
+
// The placeholders `gh api` expands to its base repository's owner before it sends the request
|
|
34
|
+
// (joshuafolkken/kit#3188) — `{owner}`, and the legacy `:owner` it still accepts. Which owner that is
|
|
35
|
+
// depends on the checkout's remotes, so the caller resolves it rather than this parser.
|
|
36
|
+
const OWNER_PLACEHOLDERS: ReadonlySet<string> = new Set(['{owner}', ':owner'])
|
|
37
|
+
|
|
33
38
|
interface RepoTarget {
|
|
34
39
|
owner: string
|
|
35
40
|
repo: string
|
|
@@ -65,6 +70,18 @@ function repo_target(segment: string): RepoTarget | undefined {
|
|
|
65
70
|
return { owner: match[OWNER_GROUP] ?? '', repo: match[REPO_GROUP] ?? '' }
|
|
66
71
|
}
|
|
67
72
|
|
|
68
|
-
|
|
73
|
+
function is_owner_placeholder(owner: string): boolean {
|
|
74
|
+
return OWNER_PLACEHOLDERS.has(owner)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const gh_api = {
|
|
78
|
+
GH_FLAGS,
|
|
79
|
+
is_gh_api,
|
|
80
|
+
is_owner_placeholder,
|
|
81
|
+
is_read,
|
|
82
|
+
is_write,
|
|
83
|
+
method_of,
|
|
84
|
+
repo_target,
|
|
85
|
+
}
|
|
69
86
|
|
|
70
87
|
export { gh_api }
|
|
@@ -6,6 +6,7 @@ import type { GuardRun } from '#scripts/josh/hook-decision'
|
|
|
6
6
|
import { lane_child_marker, type MarkerSource } from '#scripts/lane/lane-child-marker'
|
|
7
7
|
import { lane_paths } from '#scripts/lane/lane-paths'
|
|
8
8
|
import { run_cut, type RunCut } from '#scripts/run/run-cut'
|
|
9
|
+
import { run_cut_handoff } from '#scripts/run/run-cut-handoff'
|
|
9
10
|
import type { GuardedCall } from '#scripts/time-runtime/time-batch-guard'
|
|
10
11
|
import { bash_triggers } from './bash-triggers'
|
|
11
12
|
import { implementation_cut_verdict } from './implementation-cut-verdict'
|
|
@@ -161,7 +162,8 @@ const IMPLEMENTATION_CUT_REASON =
|
|
|
161
162
|
`cost has crossed the shared ${THRESHOLD_TEXT} threshold mid-implementation, so the thinking accumulated ` +
|
|
162
163
|
'so far is now re-read on every later request. Take the cut before this edit. ' +
|
|
163
164
|
'First write a handoff file with the Write tool — the user’s instruction verbatim, what you have ' +
|
|
164
|
-
'completed, what remains, and what you deliberately did not touch
|
|
165
|
+
'completed, what remains, and what you deliberately did not touch, as ' +
|
|
166
|
+
`${run_cut_handoff.HANDOFF_FORMAT} (Markdown is refused) — and pass it as \`--handoff <path>\`, ` +
|
|
165
167
|
'so the fresh process resumes on the original instruction rather than the working tree alone ' +
|
|
166
168
|
'(joshuafolkken/kit#2354); a resume that finds no instruction is refused `incomplete` rather than ' +
|
|
167
169
|
'continuing blind. ' +
|
|
@@ -26,7 +26,9 @@ import { run_tail } from './run-tail'
|
|
|
26
26
|
// but only after its preflight — the scoped checks — has passed in the calling process, so a
|
|
27
27
|
// backgrounded `ship` is killed at the turn's end before any supervisor exists: the #3022 child did
|
|
28
28
|
// exactly that and was parked as a failure. Issued in the foreground it returns at `launched` within the
|
|
29
|
-
// tool timeout, and the supervisor carries the rest.
|
|
29
|
+
// tool timeout, and the supervisor carries the rest. A backgrounded `ship` alone is no longer refused but
|
|
30
|
+
// rewritten into that foreground call (`foreground_input`, joshuafolkken/kit#3154); one chained after
|
|
31
|
+
// another long command is still refused.
|
|
30
32
|
//
|
|
31
33
|
// **Its stop-time half reads the same fact from the other side.** A backgrounded task the transcript
|
|
32
34
|
// launched but never saw finish is still running, and in a child the stop about to happen kills it —
|
|
@@ -35,29 +37,61 @@ import { run_tail } from './run-tail'
|
|
|
35
37
|
|
|
36
38
|
// The josh subcommands that run long enough to be backgrounded, as `time_shell` names them — canonical,
|
|
37
39
|
// so the `ga` / `g` / `fu` aliases are covered by the one spelling.
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
)
|
|
40
|
+
const SHIP_COMMAND = `${time_shell.JOSH_PREFIX}ship`
|
|
41
|
+
const LONG_RUNNING_COMMANDS: ReadonlySet<string> = new Set([
|
|
42
|
+
...['gate', 'git', 'followup'].map((name) => `${time_shell.JOSH_PREFIX}${name}`),
|
|
43
|
+
SHIP_COMMAND,
|
|
44
|
+
])
|
|
41
45
|
|
|
42
46
|
const BACKGROUND_KEY = 'run_in_background'
|
|
47
|
+
const TIMEOUT_KEY = 'timeout'
|
|
48
|
+
// The longest a foreground Bash call may run. A backgrounded call rarely carries a `timeout`, and one that
|
|
49
|
+
// does may exceed the foreground cap, so the rewrite sets it: left at the two-minute default, a preflight
|
|
50
|
+
// whose scoped pair outruns it is cut before `launched`, the #3027 failure the rewrite exists to avoid.
|
|
51
|
+
const FOREGROUND_TIMEOUT_MS = 600_000
|
|
43
52
|
|
|
44
53
|
function is_backgrounded(input: unknown): boolean {
|
|
45
54
|
return json_value.is_record(input) && input[BACKGROUND_KEY] === true
|
|
46
55
|
}
|
|
47
56
|
|
|
48
|
-
function
|
|
49
|
-
return time_shell.josh_commands_of(command).
|
|
57
|
+
function long_commands_of(command: string): Array<string> {
|
|
58
|
+
return time_shell.josh_commands_of(command).filter((name) => LONG_RUNNING_COMMANDS.has(name))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// A call whose long-running work is `ship` alone. In a child a foreground `ship` already detaches
|
|
62
|
+
// itself after its preflight (`run-ship-cli.ts` → `should_detach`), so the only fault in backgrounding
|
|
63
|
+
// it is the flag — and the flag is what the hook rewrites (joshuafolkken/kit#3154).
|
|
64
|
+
function ships_alone(command: string): boolean {
|
|
65
|
+
const names = long_commands_of(command)
|
|
66
|
+
|
|
67
|
+
return names.length > 0 && names.every((name) => name === SHIP_COMMAND)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function backgrounded_bash_command(call: GuardedCall): string | undefined {
|
|
71
|
+
if (call.name !== cost_blocks.BASH_TOOL || !is_backgrounded(call.input)) return undefined
|
|
72
|
+
|
|
73
|
+
return time_shell.bash_command(call.input)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function is_backgrounded_ship(call: GuardedCall): boolean {
|
|
77
|
+
const command = backgrounded_bash_command(call)
|
|
78
|
+
|
|
79
|
+
return command !== undefined && ships_alone(command)
|
|
50
80
|
}
|
|
51
81
|
|
|
52
82
|
// The call's own fields alone: a backgrounded Bash call that runs a long josh command, or the push step
|
|
53
83
|
// in either spelling. A child's foreground push is claimed too, because `run-tail` would otherwise answer
|
|
54
84
|
// it with "reissue it backgrounded" — the one reissue this row then refuses — so the child is handed the
|
|
55
|
-
// detached route on its first refusal rather than two contradictory ones.
|
|
85
|
+
// detached route on its first refusal rather than two contradictory ones. A backgrounded `ship` alone is
|
|
86
|
+
// not refused: `foreground_input` turns it into the foreground call the refusal used to ask for.
|
|
56
87
|
function is_candidate(call: GuardedCall): boolean {
|
|
57
88
|
if (run_tail.is_push_step_call(call)) return true
|
|
58
|
-
if (call.name !== cost_blocks.BASH_TOOL || !is_backgrounded(call.input)) return false
|
|
59
89
|
|
|
60
|
-
|
|
90
|
+
const command = backgrounded_bash_command(call)
|
|
91
|
+
|
|
92
|
+
if (command === undefined || is_backgrounded_ship(call)) return false
|
|
93
|
+
|
|
94
|
+
return long_commands_of(command).length > 0
|
|
61
95
|
}
|
|
62
96
|
|
|
63
97
|
// True when this session is a dispatched lane child backgrounding a long-running josh command. The call's
|
|
@@ -69,6 +103,26 @@ function is_background_long_run(
|
|
|
69
103
|
return is_candidate(call) && is_lane_child()
|
|
70
104
|
}
|
|
71
105
|
|
|
106
|
+
// **A backgrounded `ship` in a child is rewritten, not refused** (joshuafolkken/kit#3154). The refusal
|
|
107
|
+
// asked for nothing but the same call in the foreground — 14 of 41 lane `ship` refusals in one measured
|
|
108
|
+
// backlogrun were that round trip, paid at the run's largest context. The input with the flag cleared,
|
|
109
|
+
// or `undefined` for every call this does not apply to.
|
|
110
|
+
function foreground_input(
|
|
111
|
+
call: GuardedCall,
|
|
112
|
+
is_lane_child: () => boolean = () => lane_child_marker.is_child_of(process.cwd()),
|
|
113
|
+
): Record<string, unknown> | undefined {
|
|
114
|
+
if (!is_backgrounded_ship(call) || !json_value.is_record(call.input)) return undefined
|
|
115
|
+
|
|
116
|
+
if (!is_lane_child()) return undefined
|
|
117
|
+
|
|
118
|
+
return { ...call.input, [BACKGROUND_KEY]: false, [TIMEOUT_KEY]: FOREGROUND_TIMEOUT_MS }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const FOREGROUND_NOTE =
|
|
122
|
+
'lane background: this lane child backgrounded `josh ship`, which would die with the headless turn ' +
|
|
123
|
+
'before its supervisor exists — so it was run in the foreground instead, where it detaches itself ' +
|
|
124
|
+
'after the preflight. End the turn on `launched` / `busy` (joshuafolkken/kit#3154).'
|
|
125
|
+
|
|
72
126
|
// The launches `launched_ids` reads off the transcript tail that never saw their finish. A launch is
|
|
73
127
|
// read off its own tool result and a finish off the harness notice, both by `time_transcript_line`.
|
|
74
128
|
function unfinished_ids(
|
|
@@ -103,7 +157,8 @@ const LANE_BACKGROUND_REASON =
|
|
|
103
157
|
'verification done, the run parked instead of merged). Hand the gate-to-merge region to the detached ' +
|
|
104
158
|
'supervisor instead: `pnpm josh ship --detach --review "<title> #<N>"` (add `--cite <N>`) issued in ' +
|
|
105
159
|
'the foreground — its preflight runs in this process before the supervisor exists, so a backgrounded ' +
|
|
106
|
-
'`ship` dies with the turn (joshuafolkken/kit#3027
|
|
160
|
+
'`ship` dies with the turn (joshuafolkken/kit#3027; a backgrounded `ship` issued alone is moved to ' +
|
|
161
|
+
'the foreground for you, joshuafolkken/kit#3154) — then end the turn on `launched` / `busy`. ' +
|
|
107
162
|
'`.claude/skills/workflow-commands/chain-rule.md` step 0 is the single source. A ' +
|
|
108
163
|
'single check you must read before continuing runs in the foreground within the tool timeout; the ' +
|
|
109
164
|
'commit-push step does not — in a child it too belongs to the detached ship, in either spelling. This ' +
|
|
@@ -128,9 +183,11 @@ const ROW = {
|
|
|
128
183
|
}
|
|
129
184
|
|
|
130
185
|
const lane_background = {
|
|
186
|
+
FOREGROUND_NOTE,
|
|
131
187
|
LANE_BACKGROUND_REASON,
|
|
132
188
|
LANE_BACKGROUND_STOP_REASON,
|
|
133
189
|
ROW,
|
|
190
|
+
foreground_input,
|
|
134
191
|
is_background_long_run,
|
|
135
192
|
pending_agent_ids,
|
|
136
193
|
pending_background_ids,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { destructive_command } from './destructive-command'
|
|
2
|
+
import { direct_pr_create } from './direct-pr-create'
|
|
2
3
|
import { index_guard } from './index-guard'
|
|
3
4
|
import { protected_files } from './protected-files'
|
|
4
5
|
|
|
@@ -6,8 +7,15 @@ import { protected_files } from './protected-files'
|
|
|
6
7
|
// `delivered-rules.ts` spreads one entry. Each reads argv or a file path rather than a glob: an index
|
|
7
8
|
// mutation in any git spelling, a recursive forced `rm` or a destructive `gh` call, and a Read of
|
|
8
9
|
// `.env` or an edit of a consumer's `.claude/settings.json`. Their triggers are disjoint from every
|
|
9
|
-
// other row's — `git restore` is split with `worktree-mutation` by `--staged` alone.
|
|
10
|
-
|
|
10
|
+
// other row's — `git restore` is split with `worktree-mutation` by `--staged` alone. The fourth row,
|
|
11
|
+
// `direct-pr-create` (joshuafolkken/kit#3183), closes the same kind of gap for `gh pr create`, which
|
|
12
|
+
// the deny list never listed: it claims `gh pr create` and a `gh api` write to the pulls collection.
|
|
13
|
+
const ROWS = [
|
|
14
|
+
index_guard.ROW,
|
|
15
|
+
destructive_command.ROW,
|
|
16
|
+
protected_files.ROW,
|
|
17
|
+
direct_pr_create.ROW,
|
|
18
|
+
] as const
|
|
11
19
|
|
|
12
20
|
const permission_guards = { ROWS }
|
|
13
21
|
|
|
@@ -103,13 +103,13 @@ const STOP_NOTIFY_REASON =
|
|
|
103
103
|
'your previous reply. This fires only while the hold is held and no notify is on the transcript, and ' +
|
|
104
104
|
'`stop_hook_active` lets a second stop through so a run is never wedged.'
|
|
105
105
|
|
|
106
|
-
// **A clean tree that still holds is a tree the next run will trample.** `
|
|
106
|
+
// **A clean tree that still holds is a tree the next run will trample.** `working-tree-hold.md`: a stop that
|
|
107
107
|
// leaves the tree clean releases the hold; a `halfrun` pre-commit stop and a `needs-human-review` stop
|
|
108
108
|
// keep it because their tree is dirty, which is why this row is silent whenever the tree is not clean.
|
|
109
109
|
const HOLD_RELEASE_REASON =
|
|
110
110
|
'⛔ working-tree hold not released: this working tree is clean but its `run:hold` record is still in ' +
|
|
111
111
|
'place, so the next run here runs `git switch main && git pull` believing the tree is free while ' +
|
|
112
|
-
'you hold it.
|
|
112
|
+
'you hold it. The workflow-commands skill → `working-tree-hold.md`: a stop that leaves the tree clean ' +
|
|
113
113
|
'releases the hold with `pnpm josh run:release <N>` (bare for a `new` entry). A `halfrun` ' +
|
|
114
114
|
'pre-commit stop and a `needs-human-review` stop keep the hold because their tree is dirty, and a ' +
|
|
115
115
|
'`prrun` stop keeps it by its stop mark — this row is silent there. Release it, then end with a one-line confirmation that it was released — do ' +
|
|
@@ -139,11 +139,11 @@ function build_citation_reason(references: ReadonlyArray<string>): string {
|
|
|
139
139
|
|
|
140
140
|
// **An offer to file is a Tier A filing deferred to the user** (joshuafolkken/kit#2422). The judgement
|
|
141
141
|
// is already made, so the reason hands over the filing chain rather than a question; the rule itself
|
|
142
|
-
// is resident in `
|
|
142
|
+
// is resident in `observation-filing.md` and only pointed at.
|
|
143
143
|
const FILING_OFFER_REASON =
|
|
144
144
|
'⛔ filing offer: your reply offers to file an Issue instead of filing it. Filing into a ' +
|
|
145
145
|
'first-party repository is Tier A — the trigger is the judgement that it is worth filing, not the ' +
|
|
146
|
-
"run's progress (
|
|
146
|
+
"run's progress (the workflow-commands skill → `observation-filing.md`). Run " +
|
|
147
147
|
'`pnpm josh issue:file "<title>" --body-file <path> --depth <0|1|2>` — it scouts, lints, labels and ' +
|
|
148
148
|
'runs `epic:bundle` itself — then end with the one-line citation of what was filed; do not repeat ' +
|
|
149
149
|
'your previous reply. If it is not worth filing ' +
|
|
@@ -17,7 +17,7 @@ import { run_tail } from './run-tail'
|
|
|
17
17
|
// pays nothing and only a `pnpm josh git -y` spawns `git status`.
|
|
18
18
|
//
|
|
19
19
|
// **Once per run, because the exemption is a person's judgement a synchronous guard cannot read.** The
|
|
20
|
-
// non-executable-config and cosmetic-asset arms of `
|
|
20
|
+
// non-executable-config and cosmetic-asset arms of `prompts/testing-guide.md`'s "Non-runtime updates" exception stay
|
|
21
21
|
// `required` by the classifier, and a person declares them in the Step 0 work summary — which no label
|
|
22
22
|
// or transcript tail records. So a row that fired every time would wedge exactly that declared-exempt
|
|
23
23
|
// commit; refusing once and passing the reissue is the same safe direction `issue-comments` takes, and
|
|
@@ -25,34 +25,78 @@ import { shell_segments } from './shell-segments'
|
|
|
25
25
|
// A third-party write means all three: a `gh api` call, a write, and a repository target whose owner
|
|
26
26
|
// the classification calls third-party. `unknown` (an unreadable session remote, an unparseable path)
|
|
27
27
|
// is not third-party — the rule stays silent rather than refusing a write it cannot prove is external.
|
|
28
|
-
|
|
28
|
+
// The `{owner}` placeholder (joshuafolkken/kit#3188) is judged by every owner `gh` could expand it to:
|
|
29
|
+
// first-party only when all of them are, so a fork checkout whose `upstream` is another owner's still
|
|
30
|
+
// refuses the write `gh` may send there. A candidate whose owner could not be read counts as
|
|
31
|
+
// third-party here — unlike an unreadable session owner, it is a remote `gh` may really write to.
|
|
32
|
+
function is_third_party_write_segment(
|
|
33
|
+
segment: string,
|
|
34
|
+
session_owner: string | undefined,
|
|
35
|
+
resolve_placeholder_owners: () => Array<string | undefined>,
|
|
36
|
+
): boolean {
|
|
29
37
|
if (!gh_api.is_gh_api(segment) || !gh_api.is_write(segment)) return false
|
|
30
38
|
|
|
31
39
|
const target = gh_api.repo_target(segment)
|
|
32
|
-
|
|
33
40
|
if (target === undefined) return false
|
|
34
41
|
|
|
35
|
-
|
|
42
|
+
if (!gh_api.is_owner_placeholder(target.owner)) {
|
|
43
|
+
return repo_party.classify(session_owner, target.owner) === repo_party.THIRD_PARTY
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return resolve_placeholder_owners().some(
|
|
47
|
+
(owner) =>
|
|
48
|
+
owner === undefined || repo_party.classify(session_owner, owner) === repo_party.THIRD_PARTY,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A segment that makes `gh` resolve the placeholder against something other than this process's
|
|
53
|
+
// checkout: a directory change, or a `GH_REPO` assignment. The guard reads the hook's own cwd and
|
|
54
|
+
// environment, so after either one its candidate set no longer describes what `gh` will expand.
|
|
55
|
+
const GH_CONTEXT_CHANGE = /(?:^|\s)(?:cd|pushd)(?:\s|$)|(?:^|\s)(?:export\s+)?GH_REPO=/u
|
|
56
|
+
|
|
57
|
+
function unreadable_candidates(): Array<string | undefined> {
|
|
58
|
+
return [undefined]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The placeholder candidates for the segment at `index`: the checkout's own, unless an earlier
|
|
62
|
+
// segment of the same line moved `gh` elsewhere — then one unreadable candidate, refused.
|
|
63
|
+
function candidates_at(
|
|
64
|
+
segments: ReadonlyArray<string>,
|
|
65
|
+
index: number,
|
|
66
|
+
resolve_placeholder_owners: () => Array<string | undefined>,
|
|
67
|
+
): () => Array<string | undefined> {
|
|
68
|
+
const has_moved = segments.slice(0, index).some((segment) => GH_CONTEXT_CHANGE.test(segment))
|
|
69
|
+
|
|
70
|
+
return has_moved ? unreadable_candidates : resolve_placeholder_owners
|
|
36
71
|
}
|
|
37
72
|
|
|
38
73
|
// The session owner is read only when a segment is actually a `gh api` write to a repository — so an
|
|
39
|
-
// ordinary shell line, and every read, pays nothing for the local config read. `resolve_owner`
|
|
40
|
-
// injected so the suite fixes the
|
|
41
|
-
// injects `has_file`. Each segment is judged on its own, so a `gh api`
|
|
42
|
-
// command's argument is read as the write it is not, exactly as every
|
|
74
|
+
// ordinary shell line, and every read, pays nothing for the local config read. `resolve_owner` and
|
|
75
|
+
// `resolve_placeholder_owners` are injected so the suite fixes the owners without touching the real
|
|
76
|
+
// remotes, the way `file-body.ts` injects `has_file`. Each segment is judged on its own, so a `gh api`
|
|
77
|
+
// write quoted inside another command's argument is read as the write it is not, exactly as every
|
|
78
|
+
// other trigger here reads its own.
|
|
43
79
|
function writes_third_party(
|
|
44
80
|
command: string,
|
|
45
81
|
resolve_owner: () => string | undefined = repo_party.current_owner,
|
|
82
|
+
resolve_placeholder_owners: () => Array<string | undefined> = repo_party.placeholder_owners,
|
|
46
83
|
): boolean {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
84
|
+
const segments = shell_segments.segments_of(command)
|
|
85
|
+
const has_write = segments.some(
|
|
86
|
+
(segment) => gh_api.is_gh_api(segment) && gh_api.is_write(segment),
|
|
87
|
+
)
|
|
50
88
|
|
|
51
|
-
if (
|
|
89
|
+
if (!has_write) return false
|
|
52
90
|
|
|
53
91
|
const session_owner = resolve_owner()
|
|
54
92
|
|
|
55
|
-
return
|
|
93
|
+
return segments.some((segment, index) =>
|
|
94
|
+
is_third_party_write_segment(
|
|
95
|
+
segment,
|
|
96
|
+
session_owner,
|
|
97
|
+
candidates_at(segments, index, resolve_placeholder_owners),
|
|
98
|
+
),
|
|
99
|
+
)
|
|
56
100
|
}
|
|
57
101
|
|
|
58
102
|
// The instruction in the shape a refusal can carry: what the command does, why it is Tier C, and what
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type RunCarry,
|
|
11
11
|
} from './run-carry'
|
|
12
12
|
import { run_carry_args, type CountRequest, type Request } from './run-carry-args'
|
|
13
|
+
import { run_carry_conversation } from './run-carry-conversation'
|
|
13
14
|
import { run_carry_stash } from './run-carry-stash'
|
|
14
15
|
import { run_event_stream } from './run-event-stream'
|
|
15
16
|
import { run_event_stream_emit } from './run-event-stream-emit'
|
|
@@ -331,7 +332,8 @@ async function count(
|
|
|
331
332
|
|
|
332
333
|
if (blocked !== undefined) return blocked
|
|
333
334
|
|
|
334
|
-
const
|
|
335
|
+
const owned = run_carry_conversation.reclaim_owner(read.carry, request.owner)
|
|
336
|
+
const carry = run_carry.apply_change(target, owned, request.change)
|
|
335
337
|
|
|
336
338
|
await record_retrospective(request)
|
|
337
339
|
|