@joshuafolkken/kit 1.978.0 → 1.998.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/epic-commands/SKILL.md +6 -5
- package/.claude/skills/workflow-commands/SKILL.md +9 -9
- package/.claude/skills/workflow-commands/background-commands.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-child.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-steps.md +1 -1
- package/.claude/skills/workflow-commands/fullrun-steps.md +8 -8
- package/.claude/skills/workflow-commands/halfrun.md +4 -5
- package/.claude/skills/workflow-commands/issue-comments.md +1 -1
- package/.claude/skills/workflow-commands/issue-scout.md +21 -22
- package/.claude/skills/workflow-commands/kickoff.md +8 -9
- package/.claude/skills/workflow-commands/observation-filing.md +9 -10
- package/.claude/skills/workflow-commands/prerequisite.md +7 -6
- package/.claude/skills/workflow-commands/retrospective.md +5 -5
- package/.claude/skills/workflow-commands/rule-residency.md +2 -2
- package/.claude/skills/workflow-commands/split-assessment.md +4 -3
- package/.claude/skills/workflow-commands/target-repository.md +7 -6
- package/.github/workflows/sonar-qube.yml +12 -2
- package/CLAUDE.md +2 -2
- package/README.md +30 -21
- package/dist/CLAUDE.md +2 -2
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-ZYYSBNR6.js → chunk-4GDR2YTV.js} +4 -4
- package/dist/hooks/{chunk-2K52BRWV.js → chunk-56LGMJJV.js} +2 -2
- package/dist/hooks/{chunk-BNGGEY3Z.js → chunk-762BDXDU.js} +266 -327
- package/dist/hooks/{chunk-Q7LE77CO.js → chunk-7WAOP42A.js} +34 -30
- package/dist/hooks/{chunk-NXL3SBXY.js → chunk-A2H5I2VD.js} +2 -2
- package/dist/hooks/{chunk-CRGJTD6E.js → chunk-AWTFKKFW.js} +1 -1
- package/dist/hooks/{chunk-KQG4776P.js → chunk-GBJUERYK.js} +1 -1
- package/dist/hooks/{chunk-BVMSPG3M.js → chunk-KPZN42RT.js} +3 -3
- package/dist/hooks/{chunk-ZPLXLJ7O.js → chunk-PAXMLGSS.js} +42 -8
- package/dist/hooks/{chunk-F6OM6SIG.js → chunk-WMIR5SNX.js} +12 -12
- 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/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +9 -9
- package/dist/hooks/stop-guard.js +99 -27
- package/dist/josh.js +21 -6
- package/package.json +5 -1
- package/prettier/basic.d.ts +3 -0
- package/prettier/basic.js +10 -0
- package/prettier/index.js +2 -2
- package/prettier/static.js +3 -10
- package/prompts/collaboration-workflow/epic-bundle.md +8 -8
- package/prompts/collaboration-workflow/issue-template.md +5 -5
- package/prompts/collaboration-workflow/rule-delivery.md +43 -30
- package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
- package/prompts/collaboration-workflow/wip-cap.md +3 -3
- package/prompts/review.md +11 -9
- package/prompts/sonar-hotspot-handling.md +6 -0
- package/prompts/testing-guide.md +1 -1
- package/scripts/agent/agent-diagnostics.ts +1 -0
- package/scripts/agent/agent-role-profile.ts +1 -1
- package/scripts/behavior/behavior-rules.ts +8 -1
- package/scripts/build/build-claude-md.ts +13 -4
- package/scripts/delegation/delegation-policy.ts +9 -9
- package/scripts/document/document-byte-budget.ts +1 -1
- package/scripts/gate/gate-steps.ts +1 -1
- package/scripts/gate/project-checks.ts +12 -11
- package/scripts/gate/type-check-command.ts +1 -1
- package/scripts/git/git-branch.ts +14 -3
- package/scripts/git/git-issue.ts +13 -5
- package/scripts/git/git-pr.ts +16 -10
- package/scripts/git/git-preflight.ts +138 -0
- package/scripts/git/telegram-notify.ts +21 -0
- package/scripts/hooks/stop-guard.ts +2 -0
- package/scripts/init/basic-path-migration.ts +24 -0
- package/scripts/init/distributed-paths.ts +22 -0
- package/scripts/init/init-actions.ts +27 -19
- package/scripts/init/init-ai-copy.ts +86 -28
- package/scripts/init/{init-static.ts → init-basic.ts} +6 -6
- package/scripts/init/init-bootstrap.ts +4 -3
- package/scripts/init/init-install.ts +1 -1
- package/scripts/init/init-logic-templates.ts +1 -1
- package/scripts/init/init-logic.ts +1 -1
- package/scripts/init/init.ts +41 -19
- package/scripts/init/kit-setup-state.ts +30 -0
- package/scripts/init/kit-written-paths.ts +33 -0
- package/scripts/init/project-profile-cli.ts +15 -1
- package/scripts/init/project-profile.ts +41 -12
- package/scripts/init/session-language-environment.ts +83 -0
- package/scripts/init/start-exec.ts +28 -0
- package/scripts/init/start-plan.ts +35 -13
- package/scripts/init/start-prompt.ts +22 -8
- package/scripts/init/start-setup-pr.ts +138 -0
- package/scripts/init/start-steps.ts +30 -30
- package/scripts/init/start.ts +7 -3
- package/scripts/issue/issue-file-cli.ts +188 -0
- package/scripts/issue/issue-file.ts +192 -0
- package/scripts/issue/issue-lint-cli.ts +1 -1
- package/scripts/issue/issue-scout-cli.ts +41 -97
- package/scripts/issue/issue-scout-closed.ts +102 -0
- package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +12 -4
- package/scripts/josh/josh-command-map.ts +1 -0
- package/scripts/josh/josh-commands-development.ts +3 -3
- package/scripts/josh/josh-commands-issue.ts +11 -0
- package/scripts/josh/josh-commands-project.ts +7 -3
- package/scripts/lint/cspell-cached.ts +3 -3
- package/scripts/lint/format.ts +1 -1
- package/scripts/lint/lint-parallel.ts +2 -2
- package/scripts/propagate/propagate-run.ts +1 -0
- package/scripts/propagate/propagate-select.ts +72 -0
- package/scripts/propagate/propagate-targets.ts +5 -3
- package/scripts/propagate/propagate.ts +86 -24
- package/scripts/rules/bash-triggers.ts +19 -6
- package/scripts/rules/delivered-rules.ts +28 -32
- package/scripts/rules/direct-filing.ts +38 -0
- package/scripts/rules/filing-cap.ts +12 -8
- package/scripts/rules/issue-citation.ts +30 -1
- package/scripts/rules/issue-fold.ts +4 -4
- package/scripts/rules/last-prompt.ts +79 -0
- package/scripts/rules/oracle-consulted.ts +4 -4
- package/scripts/rules/oracle-firing.ts +13 -16
- package/scripts/rules/rule-body-guard.ts +6 -9
- package/scripts/rules/stop-rules.ts +8 -5
- package/scripts/safe-chain/ci-installer-pin.ts +19 -12
- package/scripts/safe-chain/project-config.ts +1 -1
- package/scripts/sync/sync-ai-files.ts +204 -0
- package/scripts/sync/sync.ts +48 -184
- package/scripts/test/josh-harness-environment.ts +1 -1
- package/scripts/test/pilot-files.ts +0 -2
- package/scripts/test/test-declared-changed.ts +1 -1
- package/scripts/test/test-declared-logic.ts +55 -14
- package/scripts/test/test-declared.ts +23 -11
- package/scripts/test/test-unit-guard.ts +6 -6
- package/scripts/time-runtime/time-shell.ts +36 -2
- package/scripts/version/fresh-metadata-cache.ts +36 -0
- package/scripts/version/latest-update.ts +2 -1
- package/scripts-ai/git-workflow.ts +10 -0
- package/templates/CLAUDE.basic.md +8 -0
- package/templates/workflows/ci.yml +27 -0
- package/scripts/rules/issue-bug-label.ts +0 -85
- package/scripts/rules/issue-filing-args.ts +0 -50
- package/scripts/rules/issue-scout.ts +0 -64
- /package/{templates/CLAUDE.static.md → dist/CLAUDE.basic.md} +0 -0
- /package/templates/{cursorrules.static → cursorrules.basic} +0 -0
- /package/templates/{pnpm-workspace.static.yaml → pnpm-workspace.basic.yaml} +0 -0
- /package/templates/{prettierignore.static → prettierignore.basic} +0 -0
|
@@ -2,7 +2,6 @@ import {
|
|
|
2
2
|
ALREADY_DONE_LABEL,
|
|
3
3
|
BREAKING_CHANGE_LABEL,
|
|
4
4
|
BUGFIX_LABEL,
|
|
5
|
-
BUG_LABEL,
|
|
6
5
|
CONTEXT_CUT_THRESHOLD,
|
|
7
6
|
ENHANCEMENT_LABEL,
|
|
8
7
|
FULL_PAGE_QUERY,
|
|
@@ -20,7 +19,6 @@ import {
|
|
|
20
19
|
git_gh_api_path,
|
|
21
20
|
git_gh_exec,
|
|
22
21
|
has_any_label,
|
|
23
|
-
has_label_name,
|
|
24
22
|
has_stderr_field,
|
|
25
23
|
lane_background,
|
|
26
24
|
lane_park,
|
|
@@ -28,6 +26,7 @@ import {
|
|
|
28
26
|
parse_json_array_or_undefined,
|
|
29
27
|
parse_json_object_safe,
|
|
30
28
|
read_json_listing,
|
|
29
|
+
repo_origin,
|
|
31
30
|
repo_party,
|
|
32
31
|
run_carry,
|
|
33
32
|
run_cut,
|
|
@@ -37,31 +36,31 @@ import {
|
|
|
37
36
|
run_watcher_guard,
|
|
38
37
|
shell_segments,
|
|
39
38
|
tail_commands
|
|
40
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-7WAOP42A.js";
|
|
41
40
|
import {
|
|
42
41
|
find_local_bin_upwards
|
|
43
42
|
} from "./chunk-3V37IID6.js";
|
|
44
43
|
import {
|
|
45
44
|
batch_outcome
|
|
46
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-A2H5I2VD.js";
|
|
47
46
|
import {
|
|
48
47
|
investigation_refusal
|
|
49
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-KPZN42RT.js";
|
|
50
49
|
import {
|
|
51
50
|
duplicate_read_outcome
|
|
52
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-4GDR2YTV.js";
|
|
53
52
|
import {
|
|
54
53
|
investigation_reads
|
|
55
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-WMIR5SNX.js";
|
|
56
55
|
import {
|
|
57
56
|
lane_guard_policy,
|
|
58
57
|
time_batch_guard
|
|
59
|
-
} from "./chunk-
|
|
58
|
+
} from "./chunk-56LGMJJV.js";
|
|
60
59
|
import {
|
|
61
60
|
hook_decision,
|
|
62
61
|
lane_child_marker,
|
|
63
62
|
run_issue_number
|
|
64
|
-
} from "./chunk-
|
|
63
|
+
} from "./chunk-GBJUERYK.js";
|
|
65
64
|
import {
|
|
66
65
|
PLATFORM_TEMP_ROOT,
|
|
67
66
|
cost_attribute,
|
|
@@ -71,14 +70,28 @@ import {
|
|
|
71
70
|
json_value,
|
|
72
71
|
lane_paths,
|
|
73
72
|
stamp_file,
|
|
74
|
-
time_density_hook,
|
|
75
73
|
time_shell
|
|
76
|
-
} from "./chunk-
|
|
74
|
+
} from "./chunk-PAXMLGSS.js";
|
|
77
75
|
|
|
78
76
|
// scripts/hooks/pretool-guard.ts
|
|
79
77
|
import { text } from "node:stream/consumers";
|
|
80
78
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
81
79
|
|
|
80
|
+
// scripts/rules/direct-filing.ts
|
|
81
|
+
var DIRECT_FILING_REASON = '\u26D4 direct filing: an Issue is filed with `pnpm josh issue:file "<title>" --body-file <path> --depth <0|1|2> [--route <route>] [--label <name>] [--repo <owner/repo>]`, never with `gh api \u2026/issues` or `gh issue create`. The command runs every filing step in order \u2014 the duplicate scout, the body lint and its classification labels, the depth and `route:` labels, the `## Origin` check when filing into another repository, and `epic:bundle` after the create \u2014 so none of them is a step to remember. When the scout prints candidates it holds the filing: read each, then reissue with `--distinct <N,\u2026>` for the ones that are separate deliverables. Reference: `docs/josh-commands.md` \u2192 "`josh issue:file`". **This rule fires on every occurrence, not once per run.**';
|
|
82
|
+
function is_any_filing(command) {
|
|
83
|
+
return bash_triggers.is_direct_filing(command) || bash_triggers.is_issue_filing(command);
|
|
84
|
+
}
|
|
85
|
+
var ROW = {
|
|
86
|
+
id: "direct-filing",
|
|
87
|
+
is_trigger: bash_triggers.on_bash_command(bash_triggers.is_direct_filing),
|
|
88
|
+
reason: DIRECT_FILING_REASON,
|
|
89
|
+
decide: () => true,
|
|
90
|
+
keeps: bash_triggers.on_bash_command(bash_triggers.is_issue_filing),
|
|
91
|
+
reaches: bash_triggers.on_bash_command(is_any_filing)
|
|
92
|
+
};
|
|
93
|
+
var direct_filing = { DIRECT_FILING_REASON, ROW };
|
|
94
|
+
|
|
82
95
|
// scripts/run/run-progress.ts
|
|
83
96
|
var DEFAULT_INTERVAL_MINUTES = 20;
|
|
84
97
|
var INTERVAL_KEY = "JOSH_PROGRESS_INTERVAL_MINUTES";
|
|
@@ -382,7 +395,7 @@ function carries_a_file_body(command, has_file = existsSync) {
|
|
|
382
395
|
return writes_existing_file(command, has_file) || has_body_interpreter(command);
|
|
383
396
|
}
|
|
384
397
|
var FILE_BODY_REASON = "\u26D4 file body in a shell command: this carries a file's new text inline \u2014 a heredoc rewriting an existing file, an interpreter (`python3 - <<`, `node -e`) whose body writes, or an in-place `perl -0pi -e`. `CLAUDE.md` forbids it because the command body is re-read on every later request of the run, so an editing script written early is billed for the rest of it (`file-edits.md` measured one run at ~2m45s spent retyping existing lines). Edit a region with the Edit tool (old / new), or pass a body by path (`--field body=@<path>`, `--body-file <path>`). A new-file creation (`cat > <new-file> <<EOF`), a read-only heredoc (`cat <<EOF`), and a short `sed -i` stay allowed. **This rule fires on every occurrence, not once per run.**";
|
|
385
|
-
var
|
|
398
|
+
var ROW2 = {
|
|
386
399
|
id: "file-body",
|
|
387
400
|
is_trigger: bash_triggers.on_bash_command((command) => carries_a_file_body(command)),
|
|
388
401
|
reason: FILE_BODY_REASON,
|
|
@@ -390,7 +403,7 @@ var ROW = {
|
|
|
390
403
|
};
|
|
391
404
|
var file_body = {
|
|
392
405
|
FILE_BODY_REASON,
|
|
393
|
-
ROW,
|
|
406
|
+
ROW: ROW2,
|
|
394
407
|
carries_a_file_body,
|
|
395
408
|
is_in_place_perl,
|
|
396
409
|
is_interpreter_write,
|
|
@@ -518,7 +531,7 @@ function is_force_or_delete(command) {
|
|
|
518
531
|
return shell_segments.segments_of(command).some((segment) => is_destructive_segment(segment));
|
|
519
532
|
}
|
|
520
533
|
var GIT_FORCE_REASON = "\u26D4 force push or branch delete: this rewrites or removes shared history, which is Tier C and needs explicit current-turn user instruction \u2014 never an AI decision, even to fix a problem you caused (`prompts/collaboration-workflow/operating-rules.md`). The `.claude/settings.json` deny list matches globs, so it misses this spelling: a combined short cluster (`git push -uf`), a `git -C <path>` prefix, and the colon-form delete (`git push origin :branch`) all pass it. This row reads the argv instead, so the spelling does not matter. For an ordinary push use `pnpm josh git`; a force push, a `git push --delete` / `:branch`, or a `git branch -d` / `-D` is the user's to run in their own terminal. **This rule fires on every occurrence, not once per run.**";
|
|
521
|
-
var
|
|
534
|
+
var ROW3 = {
|
|
522
535
|
id: "git-force",
|
|
523
536
|
is_trigger: bash_triggers.on_bash_command(is_force_or_delete),
|
|
524
537
|
reason: GIT_FORCE_REASON,
|
|
@@ -526,7 +539,7 @@ var ROW2 = {
|
|
|
526
539
|
};
|
|
527
540
|
var git_force = {
|
|
528
541
|
GIT_FORCE_REASON,
|
|
529
|
-
ROW:
|
|
542
|
+
ROW: ROW3,
|
|
530
543
|
is_branch_delete,
|
|
531
544
|
is_force_or_delete,
|
|
532
545
|
is_force_or_delete_push
|
|
@@ -1056,7 +1069,7 @@ function is_reissued_refusal(now_ms, delivered_at_ms) {
|
|
|
1056
1069
|
function decide2(_call, run2, _can_record, delivered_at_ms) {
|
|
1057
1070
|
return !is_reissued_refusal(run2.now_ms, delivered_at_ms);
|
|
1058
1071
|
}
|
|
1059
|
-
var
|
|
1072
|
+
var ROW4 = {
|
|
1060
1073
|
id: "implementation-cut",
|
|
1061
1074
|
is_trigger: is_over_threshold_edit,
|
|
1062
1075
|
reason: IMPLEMENTATION_CUT_REASON,
|
|
@@ -1066,7 +1079,7 @@ var ROW3 = {
|
|
|
1066
1079
|
var implementation_cut = {
|
|
1067
1080
|
IMPLEMENTATION_CUT_REASON,
|
|
1068
1081
|
REISSUE_WINDOW_MS,
|
|
1069
|
-
ROW:
|
|
1082
|
+
ROW: ROW4,
|
|
1070
1083
|
decide: decide2,
|
|
1071
1084
|
is_edit_tool,
|
|
1072
1085
|
is_over_threshold_edit,
|
|
@@ -1075,201 +1088,6 @@ var implementation_cut = {
|
|
|
1075
1088
|
uncut_run_issue
|
|
1076
1089
|
};
|
|
1077
1090
|
|
|
1078
|
-
// scripts/rules/issue-bug-label.ts
|
|
1079
|
-
import { readFileSync as readFileSync3 } from "node:fs";
|
|
1080
|
-
import path4 from "node:path";
|
|
1081
|
-
|
|
1082
|
-
// scripts/issue/markdown-section.ts
|
|
1083
|
-
var HEADING_PREFIX = "## ";
|
|
1084
|
-
function is_heading(line) {
|
|
1085
|
-
return line.trimStart().startsWith(HEADING_PREFIX);
|
|
1086
|
-
}
|
|
1087
|
-
function is_the_heading(line, heading) {
|
|
1088
|
-
return line.trim() === heading;
|
|
1089
|
-
}
|
|
1090
|
-
function next_fence(line, fence) {
|
|
1091
|
-
const marker = /^ {0,3}(`{3,}|~{3,})/u.exec(line)?.[1];
|
|
1092
|
-
if (fence === "") return marker;
|
|
1093
|
-
const closing = new RegExp(
|
|
1094
|
-
String.raw`^ {0,3}${fence.charAt(0)}{${String(fence.length)},}\s*$`,
|
|
1095
|
-
"u"
|
|
1096
|
-
);
|
|
1097
|
-
if (closing.test(line)) return "";
|
|
1098
|
-
return void 0;
|
|
1099
|
-
}
|
|
1100
|
-
function unfenced_lines(body) {
|
|
1101
|
-
let fence = "";
|
|
1102
|
-
const visible = [];
|
|
1103
|
-
for (const line of body.split("\n")) {
|
|
1104
|
-
const next = next_fence(line, fence);
|
|
1105
|
-
const visible_line = line.replace(/^(?: {4}|\t).*/u, "");
|
|
1106
|
-
visible.push(next === void 0 && fence === "" ? visible_line : "");
|
|
1107
|
-
fence = next ?? fence;
|
|
1108
|
-
}
|
|
1109
|
-
return visible;
|
|
1110
|
-
}
|
|
1111
|
-
function has_unfenced_line(body, text2) {
|
|
1112
|
-
return unfenced_lines(body).some((line) => line.trim() === text2);
|
|
1113
|
-
}
|
|
1114
|
-
function has_line(body, text2) {
|
|
1115
|
-
return has_unfenced_line(body, text2);
|
|
1116
|
-
}
|
|
1117
|
-
function section_lines(body, heading) {
|
|
1118
|
-
const lines = body.split("\n");
|
|
1119
|
-
const visible = unfenced_lines(body);
|
|
1120
|
-
const start = visible.findIndex((line) => is_the_heading(line, heading));
|
|
1121
|
-
if (start === -1) return [];
|
|
1122
|
-
const rest = lines.slice(start + 1);
|
|
1123
|
-
const end = visible.slice(start + 1).findIndex((line) => is_heading(line));
|
|
1124
|
-
return end === -1 ? rest : rest.slice(0, end);
|
|
1125
|
-
}
|
|
1126
|
-
var markdown_section = { section_lines, is_heading, has_line, has_unfenced_line };
|
|
1127
|
-
|
|
1128
|
-
// scripts/issue/issue-bug-label.ts
|
|
1129
|
-
var BUG_DECLARATION_LINE = "- \u7A2E\u5225: \u4E0D\u5177\u5408";
|
|
1130
|
-
function is_bug_fix(body) {
|
|
1131
|
-
const background = markdown_section.section_lines(body, "## \u80CC\u666F").join("\n");
|
|
1132
|
-
return markdown_section.has_unfenced_line(background, BUG_DECLARATION_LINE);
|
|
1133
|
-
}
|
|
1134
|
-
var issue_bug_label = { BUG_DECLARATION_LINE, is_bug_fix };
|
|
1135
|
-
|
|
1136
|
-
// scripts/issue/issue-classification.ts
|
|
1137
|
-
var FEATURE_DECLARATIONS = ["- \u76EE\u7684: \u6A5F\u80FD\u8FFD\u52A0", "- \u76EE\u7684: \u6A5F\u80FD\u6539\u5584"];
|
|
1138
|
-
var BREAKING_DECLARATION = "- \u4E92\u63DB\u6027: \u7834\u58CA\u7684\u5909\u66F4";
|
|
1139
|
-
var NON_BUG_DECLARATION = "- \u7A2E\u5225: \u975E\u4E0D\u5177\u5408";
|
|
1140
|
-
var CLASSIFICATION_LABELS = [BUG_LABEL, ENHANCEMENT_LABEL, BREAKING_CHANGE_LABEL];
|
|
1141
|
-
var CONFLICT = "bug and enhancement declarations cannot be combined";
|
|
1142
|
-
var MISSING_BUG_CLASSIFICATION = "missing bug classification";
|
|
1143
|
-
var CONFLICTING_BUG_CLASSIFICATIONS = "conflicting bug classifications";
|
|
1144
|
-
function required_labels(body) {
|
|
1145
|
-
const labels = [];
|
|
1146
|
-
const background = markdown_section.section_lines(body, "## \u80CC\u666F").join("\n");
|
|
1147
|
-
if (issue_bug_label.is_bug_fix(body)) labels.push(BUG_LABEL);
|
|
1148
|
-
if (FEATURE_DECLARATIONS.some((line) => markdown_section.has_unfenced_line(background, line))) {
|
|
1149
|
-
labels.push(ENHANCEMENT_LABEL);
|
|
1150
|
-
}
|
|
1151
|
-
if (markdown_section.has_unfenced_line(background, BREAKING_DECLARATION)) {
|
|
1152
|
-
labels.push(BREAKING_CHANGE_LABEL);
|
|
1153
|
-
}
|
|
1154
|
-
return labels;
|
|
1155
|
-
}
|
|
1156
|
-
function labels_for(body, existing) {
|
|
1157
|
-
const missing = required_labels(body).filter((label) => !has_label_name(existing, label));
|
|
1158
|
-
return [...existing, ...missing];
|
|
1159
|
-
}
|
|
1160
|
-
function bug_classification_problem(body) {
|
|
1161
|
-
const background = markdown_section.section_lines(body, "## \u80CC\u666F").join("\n");
|
|
1162
|
-
const has_bug = issue_bug_label.is_bug_fix(body);
|
|
1163
|
-
const has_non_bug = markdown_section.has_unfenced_line(background, NON_BUG_DECLARATION);
|
|
1164
|
-
if (!has_bug && !has_non_bug) return MISSING_BUG_CLASSIFICATION;
|
|
1165
|
-
if (has_bug && has_non_bug) return CONFLICTING_BUG_CLASSIFICATIONS;
|
|
1166
|
-
return void 0;
|
|
1167
|
-
}
|
|
1168
|
-
function problems(body) {
|
|
1169
|
-
const classification_problem = bug_classification_problem(body);
|
|
1170
|
-
const found = [];
|
|
1171
|
-
if (classification_problem !== void 0) found.push(classification_problem);
|
|
1172
|
-
const labels = required_labels(body);
|
|
1173
|
-
if (labels.includes(BUG_LABEL) && labels.includes(ENHANCEMENT_LABEL)) found.push(CONFLICT);
|
|
1174
|
-
return found;
|
|
1175
|
-
}
|
|
1176
|
-
var issue_classification = {
|
|
1177
|
-
BREAKING_DECLARATION,
|
|
1178
|
-
CLASSIFICATION_LABELS,
|
|
1179
|
-
FEATURE_DECLARATIONS,
|
|
1180
|
-
NON_BUG_DECLARATION,
|
|
1181
|
-
labels_for,
|
|
1182
|
-
problems,
|
|
1183
|
-
required_labels
|
|
1184
|
-
};
|
|
1185
|
-
|
|
1186
|
-
// scripts/rules/issue-filing-args.ts
|
|
1187
|
-
var SHELL_WORD = /(?:[^\s'"]|'[^']*'|"[^"]*")+/gu;
|
|
1188
|
-
var QUOTE = /['"]/gu;
|
|
1189
|
-
var RAW_FIELDS = /* @__PURE__ */ new Set(["-f", "--raw-field"]);
|
|
1190
|
-
var FILE_FIELDS = /* @__PURE__ */ new Set(["-F", "--field"]);
|
|
1191
|
-
var LABEL_OPTIONS = /* @__PURE__ */ new Set(["-l", "--label"]);
|
|
1192
|
-
var BODY_FILE_OPTIONS = /* @__PURE__ */ new Set(["--body-file"]);
|
|
1193
|
-
var INLINE_BODY_OPTIONS = /* @__PURE__ */ new Set(["-b", "--body"]);
|
|
1194
|
-
var BODY_FILE_PREFIX = "body=@";
|
|
1195
|
-
function words_of2(command) {
|
|
1196
|
-
return [...command.matchAll(SHELL_WORD)].map((match) => match[0].replaceAll(QUOTE, ""));
|
|
1197
|
-
}
|
|
1198
|
-
function values_after(words, flags) {
|
|
1199
|
-
return words.flatMap((word, index) => {
|
|
1200
|
-
const value = words[index + 1];
|
|
1201
|
-
return value !== void 0 && flags.has(word) ? [value] : [];
|
|
1202
|
-
});
|
|
1203
|
-
}
|
|
1204
|
-
function has_inline_body(words) {
|
|
1205
|
-
const has_raw = values_after(words, RAW_FIELDS).some((value) => value.startsWith("body="));
|
|
1206
|
-
return has_raw || values_after(words, INLINE_BODY_OPTIONS).length > 0;
|
|
1207
|
-
}
|
|
1208
|
-
function body_file(command) {
|
|
1209
|
-
const words = words_of2(command);
|
|
1210
|
-
const fields = values_after(words, FILE_FIELDS).filter((value) => value.startsWith("body="));
|
|
1211
|
-
const options = values_after(words, BODY_FILE_OPTIONS);
|
|
1212
|
-
if (has_inline_body(words) || fields.length + options.length !== 1) return void 0;
|
|
1213
|
-
return fields[0]?.startsWith(BODY_FILE_PREFIX) ? fields[0].slice(BODY_FILE_PREFIX.length) : options[0];
|
|
1214
|
-
}
|
|
1215
|
-
function has_label(command, label) {
|
|
1216
|
-
const words = words_of2(command);
|
|
1217
|
-
const fields = [...values_after(words, RAW_FIELDS), ...values_after(words, FILE_FIELDS)];
|
|
1218
|
-
const options = values_after(words, LABEL_OPTIONS).flatMap((value) => value.split(","));
|
|
1219
|
-
return fields.includes(`labels[]=${label}`) || options.includes(label);
|
|
1220
|
-
}
|
|
1221
|
-
var issue_filing_args = { body_file, has_label, words_of: words_of2 };
|
|
1222
|
-
|
|
1223
|
-
// scripts/rules/issue-bug-label.ts
|
|
1224
|
-
var LINT_NAMES = /* @__PURE__ */ new Set(["issue:lint"]);
|
|
1225
|
-
var LINT_PATH = /\b(?:issue:lint|iln)\s+(?:'([^']+)'|"([^"]+)"|(\S+))/u;
|
|
1226
|
-
function lint_argument(segment) {
|
|
1227
|
-
const [, single_quoted, double_quoted, bare] = LINT_PATH.exec(segment) ?? [];
|
|
1228
|
-
return single_quoted ?? double_quoted ?? bare;
|
|
1229
|
-
}
|
|
1230
|
-
function latest_lint_path(tail) {
|
|
1231
|
-
const segments = tail_commands.prior_bash_commands(tail).flatMap((command) => shell_segments.segments_of(command));
|
|
1232
|
-
const lint = segments.findLast((segment) => shell_segments.is_josh_command(segment, LINT_NAMES));
|
|
1233
|
-
return lint === void 0 ? void 0 : lint_argument(lint);
|
|
1234
|
-
}
|
|
1235
|
-
function linted_body(body_path) {
|
|
1236
|
-
try {
|
|
1237
|
-
return readFileSync3(body_path, "utf8");
|
|
1238
|
-
} catch {
|
|
1239
|
-
return void 0;
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
function matches_linted_file(command, lint_path) {
|
|
1243
|
-
const filing_path = issue_filing_args.body_file(command);
|
|
1244
|
-
return filing_path !== void 0 && path4.resolve(filing_path) === path4.resolve(lint_path);
|
|
1245
|
-
}
|
|
1246
|
-
function needs_classification_labels(command, tail) {
|
|
1247
|
-
const lint_path = latest_lint_path(tail);
|
|
1248
|
-
if (lint_path === void 0) return false;
|
|
1249
|
-
const body = linted_body(lint_path);
|
|
1250
|
-
if (body === void 0) return false;
|
|
1251
|
-
if (!matches_linted_file(command, lint_path) || issue_classification.problems(body).length > 0) {
|
|
1252
|
-
return true;
|
|
1253
|
-
}
|
|
1254
|
-
const expected = issue_classification.required_labels(body);
|
|
1255
|
-
return issue_classification.CLASSIFICATION_LABELS.some(
|
|
1256
|
-
(label) => expected.includes(label) !== issue_filing_args.has_label(command, label)
|
|
1257
|
-
);
|
|
1258
|
-
}
|
|
1259
|
-
function decide3(call, run2) {
|
|
1260
|
-
const command = time_shell.bash_command(call.input);
|
|
1261
|
-
const tail = time_density_hook.read_tail(run2.transcript);
|
|
1262
|
-
return needs_classification_labels(command, tail);
|
|
1263
|
-
}
|
|
1264
|
-
var ISSUE_CLASSIFICATION_REASON = "\u26D4 issue classification mismatch: use the exact body file checked by `pnpm josh issue:lint` in this creation call (`-F body=@<path>` or `gh issue create --body-file <path>`) and apply exactly the `labels:` it reports (`bug`, `enhancement`, `breaking-change`). See `prompts/collaboration-workflow/issue-template.md`.";
|
|
1265
|
-
var ROW4 = {
|
|
1266
|
-
id: "issue-bug-label",
|
|
1267
|
-
is_trigger: bash_triggers.on_bash_command(bash_triggers.is_issue_filing),
|
|
1268
|
-
reason: ISSUE_CLASSIFICATION_REASON,
|
|
1269
|
-
decide: decide3
|
|
1270
|
-
};
|
|
1271
|
-
var issue_bug_label_rule = { ROW: ROW4, latest_lint_path, needs_classification_labels };
|
|
1272
|
-
|
|
1273
1091
|
// scripts/rules/issue-fold.ts
|
|
1274
1092
|
var FOLD_NAMES = /* @__PURE__ */ new Set(["issue:fold"]);
|
|
1275
1093
|
function runs_the_fold(command) {
|
|
@@ -1279,7 +1097,7 @@ function already_folded(tail, _call) {
|
|
|
1279
1097
|
if (filing_cap.prior_filing_count(tail) === 0) return true;
|
|
1280
1098
|
return tail_commands.prior_bash_commands(tail).some((command) => runs_the_fold(command));
|
|
1281
1099
|
}
|
|
1282
|
-
var ISSUE_FOLD_REASON = '\u26D4 issue fold: this run has filed an Issue before and is filing another with no `pnpm josh issue:fold` between them. Several findings from one session fold into one Issue by default \u2014 the filing-time counterpart to the split assessment, whose two questions it reuses: are the findings separable, and does the whole clearly exceed one verification gate (the size half is `split:assess`, called not recomputed). Reissue after folding: `pnpm josh issue:fold "<title>" "<title>" \u2026` \u2014 `fold` means file one Issue covering them, `separate` means file them apart (separable findings whose combined size clears the split guide), and the default is `fold`. Add `--not-separable` when they are really one deliverable. The procedure is `.claude/skills/workflow-commands/split-assessment.md` \u2192 "The question".
|
|
1100
|
+
var ISSUE_FOLD_REASON = '\u26D4 issue fold: this run has filed an Issue before and is filing another with no `pnpm josh issue:fold` between them. Several findings from one session fold into one Issue by default \u2014 the filing-time counterpart to the split assessment, whose two questions it reuses: are the findings separable, and does the whole clearly exceed one verification gate (the size half is `split:assess`, called not recomputed). Reissue after folding: `pnpm josh issue:fold "<title>" "<title>" \u2026` \u2014 `fold` means file one Issue covering them, `separate` means file them apart (separable findings whose combined size clears the split guide), and the default is `fold`. Add `--not-separable` when they are really one deliverable. The procedure is `.claude/skills/workflow-commands/split-assessment.md` \u2192 "The question". Every later filing is refused until the fold is on the transcript.';
|
|
1283
1101
|
var ROW5 = {
|
|
1284
1102
|
id: "issue-fold",
|
|
1285
1103
|
is_trigger: bash_triggers.on_bash_command(bash_triggers.is_issue_filing),
|
|
@@ -1289,24 +1107,6 @@ var ROW5 = {
|
|
|
1289
1107
|
};
|
|
1290
1108
|
var issue_fold = { ISSUE_FOLD_REASON, ROW: ROW5, already_folded, runs_the_fold };
|
|
1291
1109
|
|
|
1292
|
-
// scripts/rules/issue-scout.ts
|
|
1293
|
-
var SCOUT_NAMES = /* @__PURE__ */ new Set(["issue:scout"]);
|
|
1294
|
-
function runs_the_scout(command) {
|
|
1295
|
-
return shell_segments.segments_of(command).some((segment) => shell_segments.is_josh_command(segment, SCOUT_NAMES));
|
|
1296
|
-
}
|
|
1297
|
-
function already_scouted(tail, _call) {
|
|
1298
|
-
return tail_commands.prior_bash_commands(tail).some((command) => runs_the_scout(command));
|
|
1299
|
-
}
|
|
1300
|
-
var ISSUE_SCOUT_REASON = '\u26D4 issue scout: this files an Issue with no `pnpm josh issue:scout` earlier in the run. A duplicate is likeliest for the work that just finished, which is exactly where self-restraint is weakest. Reissue after scouting: `pnpm josh issue:scout "<title>"` (add `--body "<one-line summary, citing #N where the work follows one>"` so the epic half can answer), then read its `Duplicates:` \u2014 an **open** candidate covering the same work stops the run rather than filing a second Issue, and a `(closed)` one means the work is already done \u2014 before reissuing this filing. The procedure is `.claude/skills/workflow-commands/SKILL.md` \u2192 \xA72e. It fires once per run and cannot repeat on the call in hand.';
|
|
1301
|
-
var ROW6 = {
|
|
1302
|
-
id: "issue-scout",
|
|
1303
|
-
is_trigger: bash_triggers.on_bash_command(bash_triggers.is_issue_filing),
|
|
1304
|
-
reason: ISSUE_SCOUT_REASON,
|
|
1305
|
-
already_satisfied: already_scouted,
|
|
1306
|
-
keeps: bash_triggers.on_bash_command(runs_the_scout)
|
|
1307
|
-
};
|
|
1308
|
-
var issue_scout = { ISSUE_SCOUT_REASON, ROW: ROW6, already_scouted, runs_the_scout };
|
|
1309
|
-
|
|
1310
1110
|
// scripts/rules/josh-git-bare.ts
|
|
1311
1111
|
var GIT_COMMAND2 = /* @__PURE__ */ new Set(["git"]);
|
|
1312
1112
|
var CONFIRM_FLAGS = /* @__PURE__ */ new Set(["-y", "--yes"]);
|
|
@@ -1321,7 +1121,7 @@ function is_bare_josh_git(command) {
|
|
|
1321
1121
|
return shell_segments.segments_of(command).some((segment) => is_bare_segment(segment));
|
|
1322
1122
|
}
|
|
1323
1123
|
var JOSH_GIT_BARE_REASON = '\u26D4 bare `pnpm josh git`: this prompts to confirm the staging, and with no TTY the prompt falls to its cancel branch \u2014 `Checking unstaged files... Found` then `Operation cancelled.` \u2014 so the call throws away the time it takes to fail (about 31 seconds in one measured lane) and you reissue it with `-y` anyway (joshuafolkken/kit#2297). Run it unattended from the start: `pnpm josh git -y "<title> #<N>"`. The recovery form that opens the PR for a push that already landed is `pnpm josh git -y --skip-commit --skip-push`. This rule fires on every occurrence, not once per run.';
|
|
1324
|
-
var
|
|
1124
|
+
var ROW6 = {
|
|
1325
1125
|
id: "josh-git-bare",
|
|
1326
1126
|
is_trigger: bash_triggers.on_bash_command(is_bare_josh_git),
|
|
1327
1127
|
reason: JOSH_GIT_BARE_REASON,
|
|
@@ -1329,7 +1129,7 @@ var ROW7 = {
|
|
|
1329
1129
|
};
|
|
1330
1130
|
var josh_git_bare = {
|
|
1331
1131
|
JOSH_GIT_BARE_REASON,
|
|
1332
|
-
ROW:
|
|
1132
|
+
ROW: ROW6,
|
|
1333
1133
|
is_bare_josh_git
|
|
1334
1134
|
};
|
|
1335
1135
|
|
|
@@ -1343,7 +1143,7 @@ function is_carry_conflict(command, is_lane_child = lane_child_marker.is_child_o
|
|
|
1343
1143
|
return is_lane_child;
|
|
1344
1144
|
}
|
|
1345
1145
|
var LANE_CARRY_CONFLICT_REASON = '\u26D4 lane child carry conflict: this session is a dispatched lane child, and `pnpm josh run:merge` / `pnpm josh run:carry` are the parent backlogrun budget commands \u2014 a child must never run them. The live carry record here belongs to the parent that launched this child, not to a competing run: the parent holds the one run budget for the repository, and every lane shares the common git directory it is keyed on, so a live owner a lane child reads is always its parent (joshuafolkken/kit#2267, measured 2026-09-21 in backlogrun #2252 \u2014 a child read run:merge `busy` as a competitor and parked #2258 unimplemented). Do not run this command, and do not treat it as a conflict: no `needs-decision`, no `confirmation` Telegram, no question to a person \u2014 a dispatched child cannot ask, and there is nothing to decide. Continue implementing this Issue; the parent owns the budget and counts the outcome at the return. The procedure is `.claude/skills/workflow-commands/pre-gate-cut.md` and `backlogrun-progress.md` \u2192 "The hand-off". This rule fires on every occurrence, not once per run.';
|
|
1346
|
-
var
|
|
1146
|
+
var ROW7 = {
|
|
1347
1147
|
id: "lane-carry-conflict",
|
|
1348
1148
|
is_trigger: bash_triggers.on_bash_command(is_carry_conflict),
|
|
1349
1149
|
reason: LANE_CARRY_CONFLICT_REASON,
|
|
@@ -1351,7 +1151,7 @@ var ROW8 = {
|
|
|
1351
1151
|
};
|
|
1352
1152
|
var lane_carry_conflict = {
|
|
1353
1153
|
LANE_CARRY_CONFLICT_REASON,
|
|
1354
|
-
ROW:
|
|
1154
|
+
ROW: ROW7,
|
|
1355
1155
|
invokes_budget_command,
|
|
1356
1156
|
is_carry_conflict
|
|
1357
1157
|
};
|
|
@@ -1404,13 +1204,13 @@ function is_interactive_ask(call, is_lane_child = lane_child_marker.is_child_of(
|
|
|
1404
1204
|
return is_lane_child;
|
|
1405
1205
|
}
|
|
1406
1206
|
var LANE_INTERACTIVE_ASK_REASON = '\u26D4 lane child interactive ask: this session is a dispatched lane child and cannot ask a person, so this call is refused. Left to run it would end the turn with nothing on the Issue \u2014 the parent then finds an OPEN Issue with `in-progress` and no question (joshuafolkken/kit#2201; joshuafolkken/kit#2034 placed the same rule one tool-call too late). Do not ask: park the decision instead. Apply `needs-decision` and post a comment carrying the question, the options and whether work was stashed \u2014 `gh api repos/{owner}/{repo}/issues/<N>/labels -f \'labels[]=needs-decision\'` then `pnpm josh issue:comment <N> --body-file <path>` \u2014 then send the `confirmation` Telegram and stop. The procedure is `.claude/skills/workflow-commands/pre-gate-cut.md` \u2192 "A lane child records its park before it stops", and the park itself is `backlogrun-park.md` \u2192 "park and continue". This rule fires on every occurrence, not once per run.';
|
|
1407
|
-
var
|
|
1207
|
+
var ROW8 = {
|
|
1408
1208
|
id: "lane-interactive-ask",
|
|
1409
1209
|
is_trigger: is_interactive_ask,
|
|
1410
1210
|
reason: LANE_INTERACTIVE_ASK_REASON,
|
|
1411
1211
|
decide: () => true
|
|
1412
1212
|
};
|
|
1413
|
-
var lane_interactive_ask = { LANE_INTERACTIVE_ASK_REASON, ROW:
|
|
1213
|
+
var lane_interactive_ask = { LANE_INTERACTIVE_ASK_REASON, ROW: ROW8, is_interactive_ask };
|
|
1414
1214
|
|
|
1415
1215
|
// scripts/rules/lane-switch-main.ts
|
|
1416
1216
|
var SWITCH_SUBCOMMAND = "switch";
|
|
@@ -1441,7 +1241,7 @@ function is_switch_away_from_lane(command, is_lane_child = lane_child_marker.is_
|
|
|
1441
1241
|
return switch_targets(command).some((target) => target !== own_branch);
|
|
1442
1242
|
}
|
|
1443
1243
|
var LANE_SWITCH_MAIN_REASON = "\u26D4 lane child on `git switch main`: this session is a dispatched lane child, and switching to the default branch here always fails \u2014 a lane is a linked work tree, the primary checkout holds the default branch, and git allows one branch in one work tree at a time (`fatal: main is already used by worktree at <the primary checkout>`, measured 11 times in the 2026-09-21 backlogrun). Do not switch: the lane is already on its own issue branch, branched from a fresh default before it opened, and the latest default is brought in during the gate by `pnpm josh main:merge`. So skip this step and keep implementing on the current branch \u2014 no `git switch`, no `git pull`. `pnpm josh main:sync` (`josh ms`) is refused in a lane for the same reason, and a lane is finished with `pnpm josh lane:close <issue-number>`. The parent runs `git switch main && git pull` in the primary checkout before each lane opens, never the child \u2014 `backlogrun-lanes.md` and `backlogrun-child.md` are the single sources. This rule fires on every occurrence, not once per run.";
|
|
1444
|
-
var
|
|
1244
|
+
var ROW9 = {
|
|
1445
1245
|
id: "lane-switch-main",
|
|
1446
1246
|
is_trigger: bash_triggers.on_bash_command(is_switch_away_from_lane),
|
|
1447
1247
|
reason: LANE_SWITCH_MAIN_REASON,
|
|
@@ -1449,7 +1249,7 @@ var ROW10 = {
|
|
|
1449
1249
|
};
|
|
1450
1250
|
var lane_switch_main = {
|
|
1451
1251
|
LANE_SWITCH_MAIN_REASON,
|
|
1452
|
-
ROW:
|
|
1252
|
+
ROW: ROW9,
|
|
1453
1253
|
is_switch_away_from_lane,
|
|
1454
1254
|
switch_target,
|
|
1455
1255
|
switch_targets
|
|
@@ -1752,20 +1552,13 @@ var FIRING = /* @__PURE__ */ new Map([
|
|
|
1752
1552
|
describes: "a package-add command (`pnpm add \u2026`, `npm install <pkg>`)",
|
|
1753
1553
|
governs: is_package_add
|
|
1754
1554
|
}
|
|
1755
|
-
],
|
|
1756
|
-
[
|
|
1757
|
-
"issue:lint",
|
|
1758
|
-
{
|
|
1759
|
-
describes: "an Issue-filing call (`gh api \u2026/issues`, `gh issue create`)",
|
|
1760
|
-
governs: bash_triggers.is_issue_filing
|
|
1761
|
-
}
|
|
1762
1555
|
]
|
|
1763
1556
|
]);
|
|
1764
1557
|
var PHASE = "the act it governs is a phase of a run, not a single shell call a trigger can anchor on";
|
|
1765
1558
|
var READ = "it answers a status question and governs no action of its own to gate before";
|
|
1766
1559
|
var SELF = "the command is itself the governed act; there is no earlier call to gate before it";
|
|
1767
1560
|
var LATEST_SCOPE_REASON = "the run it governs (`pnpm josh latest`) is already gated at its point of use by the latest-gate; a firing here would double-gate it";
|
|
1768
|
-
var
|
|
1561
|
+
var FILING_STEP_REASON = "`pnpm josh issue:file` runs it as one of its filing steps, and a direct filing is refused by the `direct-filing` delivered rule; there is no separate call left to gate";
|
|
1769
1562
|
var RELEASE_SCOPE_REASON = "it reads the release owed *after* `pnpm josh followup` has merged (`git_followup_pending`), so it trails the merge rather than gating it; a firing on `followup` would gate a check that must run once the merge is done, reading a pre-merge state that has not yet arrived";
|
|
1770
1563
|
var NOT_NAMED = /* @__PURE__ */ new Map([
|
|
1771
1564
|
["release:scope", RELEASE_SCOPE_REASON],
|
|
@@ -1777,8 +1570,9 @@ var NOT_NAMED = /* @__PURE__ */ new Map([
|
|
|
1777
1570
|
["latest:scope", LATEST_SCOPE_REASON],
|
|
1778
1571
|
["run:hold", SELF],
|
|
1779
1572
|
["cost:cut", READ],
|
|
1780
|
-
["epic:bundle",
|
|
1781
|
-
["issue:scout",
|
|
1573
|
+
["epic:bundle", FILING_STEP_REASON],
|
|
1574
|
+
["issue:scout", FILING_STEP_REASON],
|
|
1575
|
+
["issue:lint", FILING_STEP_REASON],
|
|
1782
1576
|
[
|
|
1783
1577
|
"issue:fold",
|
|
1784
1578
|
"the second filing it governs is already gated by the `issue-fold` delivered rule"
|
|
@@ -1834,7 +1628,7 @@ function command_line(oracle) {
|
|
|
1834
1628
|
return oracle.args ? `pnpm josh ${command} ${oracle.args}` : `pnpm josh ${command}`;
|
|
1835
1629
|
}
|
|
1836
1630
|
function reason_for(oracle, firing_point) {
|
|
1837
|
-
return `\u26D4 decision oracle not consulted: this is ${firing_point.describes}, but the run has not run \`${command_line(oracle)}\` first \u2014 the oracle that decides ${oracle.decision.toLowerCase()}. It answers ${oracle.vocabulary.join(" | ")}; single source ${oracle.single_source}. Reissue after running it and reading its verdict.
|
|
1631
|
+
return `\u26D4 decision oracle not consulted: this is ${firing_point.describes}, but the run has not run \`${command_line(oracle)}\` first \u2014 the oracle that decides ${oracle.decision.toLowerCase()}. It answers ${oracle.vocabulary.join(" | ")}; single source ${oracle.single_source}. Reissue after running it and reading its verdict. Every such call is refused until that command is on the transcript.`;
|
|
1838
1632
|
}
|
|
1839
1633
|
function row_for(oracle, firing_point) {
|
|
1840
1634
|
const names = command_names(oracle);
|
|
@@ -1917,7 +1711,7 @@ function is_poll_loop(command) {
|
|
|
1917
1711
|
return LOOP_KEYWORD2.test(plain) && SLEEP_COMMAND.test(plain);
|
|
1918
1712
|
}
|
|
1919
1713
|
var POLL_LOOP_REASON = '\u26D4 output poll loop: this hand-writes a `while`/`until` wait loop over a command\'s output, a marker file or a process \u2014 with a `sleep` or, worse, without one \u2014 and the loop does not know when the command finished. A regex that never matches waits to the harness block limit; one that matches a mid-run line exits too early \u2014 either way the command exit is not what ends the wait, and each pass spends a full-context round trip (joshuafolkken/kit#2371 measured about 45 minutes, 13% of six lanes, lost to exactly this). Do not poll the output file. A command known to take minutes \u2014 `pnpm josh gate`, `pnpm josh followup`, `pnpm josh format` \u2014 is started detached from the first with `run_in_background` set, and the harness re-invokes you when it exits: its completion notification is what ends the wait, on the task exit rather than a regex or the block limit. So issue the long command in the background and end the turn \u2014 do not arm a loop; the wake-up brings you back. To glance at interim output, `Read` the output file once, never in a sleep loop. If you are waiting on CI, `pnpm josh followup` waits on it for you. The procedure is `.claude/skills/workflow-commands/background-commands.md`, "A command that can take minutes is issued in the background". **This rule fires on every occurrence, not once per run.**';
|
|
1920
|
-
var
|
|
1714
|
+
var ROW10 = {
|
|
1921
1715
|
id: "poll-loop",
|
|
1922
1716
|
is_trigger: bash_triggers.on_bash_command(is_poll_loop),
|
|
1923
1717
|
reason: POLL_LOOP_REASON,
|
|
@@ -1925,7 +1719,7 @@ var ROW11 = {
|
|
|
1925
1719
|
};
|
|
1926
1720
|
var poll_loop = {
|
|
1927
1721
|
POLL_LOOP_REASON,
|
|
1928
|
-
ROW:
|
|
1722
|
+
ROW: ROW10,
|
|
1929
1723
|
is_poll_loop
|
|
1930
1724
|
};
|
|
1931
1725
|
|
|
@@ -2045,16 +1839,16 @@ function posts_at_path_literally(command) {
|
|
|
2045
1839
|
return RAW_FIELD_BODY_AT.test(command);
|
|
2046
1840
|
}
|
|
2047
1841
|
var RAW_FIELD_BODY_REASON = "\u26D4 literal @path body: this passes `body=@<path>` to `gh api` with a raw field flag (`-f` / `--raw-field`), which sends the value verbatim \u2014 the string `@<path>` is posted as the comment, not the file it points at. `-F` / `--field` is the flag that reads the file, and the two spellings differ by one character while `gh` exits 0 and returns a comment URL either way, so the break is invisible until a person reads the issue (joshuafolkken/kit#2304 lost two park comments this way). Post an issue or PR comment with `pnpm josh issue:comment <N> --body-file <path>` \u2014 the one spelling, which cannot take the wrong flag. The rule and the safe forms are in `prompts/collaboration-workflow/shell-body.md`. This rule fires on every occurrence, not once per run.";
|
|
2048
|
-
var
|
|
1842
|
+
var ROW11 = {
|
|
2049
1843
|
id: "raw-field-body",
|
|
2050
1844
|
is_trigger: bash_triggers.on_bash_command(posts_at_path_literally),
|
|
2051
1845
|
reason: RAW_FIELD_BODY_REASON,
|
|
2052
1846
|
decide: () => true
|
|
2053
1847
|
};
|
|
2054
|
-
var raw_field_body = { RAW_FIELD_BODY_REASON, ROW:
|
|
1848
|
+
var raw_field_body = { RAW_FIELD_BODY_REASON, ROW: ROW11, posts_at_path_literally };
|
|
2055
1849
|
|
|
2056
1850
|
// scripts/rules/rule-body-guard.ts
|
|
2057
|
-
import { existsSync as existsSync2, readFileSync as
|
|
1851
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
2058
1852
|
var EDIT_TOOL = "Edit";
|
|
2059
1853
|
var WRITE_TOOL = "Write";
|
|
2060
1854
|
var CLAUDE_DOCUMENT = /(?:^|\/)CLAUDE\.md$/u;
|
|
@@ -2077,7 +1871,7 @@ function string_field(input, key) {
|
|
|
2077
1871
|
return typeof value === "string" ? value : "";
|
|
2078
1872
|
}
|
|
2079
1873
|
function read_existing(file_path) {
|
|
2080
|
-
return existsSync2(file_path) ?
|
|
1874
|
+
return existsSync2(file_path) ? readFileSync3(file_path, "utf8") : "";
|
|
2081
1875
|
}
|
|
2082
1876
|
function texts_of(name, input, read2) {
|
|
2083
1877
|
if (name === WRITE_TOOL) {
|
|
@@ -2104,7 +1898,7 @@ function writes_rule_prose(call, read2 = read_existing) {
|
|
|
2104
1898
|
const { old_text, new_text } = texts_of(call.name, input, read2);
|
|
2105
1899
|
return adds_rule_prose(old_text, new_text);
|
|
2106
1900
|
}
|
|
2107
|
-
var RULE_BODY_REASON = "\u26D4 rule written into prose: this edit adds rule text to CLAUDE.md, a `prompts/**` doc or a skill body. Before it lands, run the residency questions the addition skips. A rule whose answer is computable from mechanically readable inputs should be a decision oracle, not prose \u2014 question 0; `pnpm josh oracle:list` lists the existing oracles a new one joins (single source `scripts/rules/decision-oracle.ts`). A rule that decides *when* or *in what order* to act belongs in the run driver, not prose \u2014 the ordering question; `pnpm josh run:step` is the state machine a sequencing rule moves into. Only a rule that is neither \u2014 not computable, not ordering \u2014 stays prose, and then as a trigger-plus-pointer line rather than a second copy of its procedure. The criterion is `prompts/collaboration-workflow/residency.md` (question 0 and the ordering question); the delivery enumeration is `prompts/collaboration-workflow/rule-delivery.md`.
|
|
1901
|
+
var RULE_BODY_REASON = "\u26D4 rule written into prose: this edit adds rule text to CLAUDE.md, a `prompts/**` doc or a skill body. Before it lands, run the residency questions the addition skips. A rule whose answer is computable from mechanically readable inputs should be a decision oracle, not prose \u2014 question 0; `pnpm josh oracle:list` lists the existing oracles a new one joins (single source `scripts/rules/decision-oracle.ts`). A rule that decides *when* or *in what order* to act belongs in the run driver, not prose \u2014 the ordering question; `pnpm josh run:step` is the state machine a sequencing rule moves into. Only a rule that is neither \u2014 not computable, not ordering \u2014 stays prose, and then as a trigger-plus-pointer line rather than a second copy of its procedure. The criterion is `prompts/collaboration-workflow/residency.md` (question 0 and the ordering question); the delivery enumeration is `prompts/collaboration-workflow/rule-delivery.md`. Every such edit is refused until both commands are on the transcript \u2014 run them, confirm the rule can be neither an oracle nor an ordering step, then reissue this edit.";
|
|
2108
1902
|
var PLACEMENT_ORACLES = [
|
|
2109
1903
|
/* @__PURE__ */ new Set(["oracle:list"]),
|
|
2110
1904
|
/* @__PURE__ */ new Set(["run:step", "run:next"])
|
|
@@ -2123,7 +1917,7 @@ function runs_placement_oracle(command) {
|
|
|
2123
1917
|
(names) => shell_segments.segments_of(command).some((segment) => shell_segments.is_josh_command(segment, names))
|
|
2124
1918
|
);
|
|
2125
1919
|
}
|
|
2126
|
-
var
|
|
1920
|
+
var ROW12 = {
|
|
2127
1921
|
id: "rule-body",
|
|
2128
1922
|
is_trigger: writes_rule_prose,
|
|
2129
1923
|
reason: RULE_BODY_REASON,
|
|
@@ -2132,7 +1926,7 @@ var ROW13 = {
|
|
|
2132
1926
|
};
|
|
2133
1927
|
var rule_body_guard = {
|
|
2134
1928
|
RULE_BODY_REASON,
|
|
2135
|
-
ROW:
|
|
1929
|
+
ROW: ROW12,
|
|
2136
1930
|
adds_rule_prose,
|
|
2137
1931
|
answered_placement,
|
|
2138
1932
|
is_rule_document,
|
|
@@ -2216,9 +2010,133 @@ var shell_body_trigger = {
|
|
|
2216
2010
|
import { execFileSync } from "node:child_process";
|
|
2217
2011
|
|
|
2218
2012
|
// scripts/gate/project-checks.ts
|
|
2219
|
-
import { existsSync as
|
|
2013
|
+
import { existsSync as existsSync4, globSync as globSync2, readFileSync as readFileSync5 } from "node:fs";
|
|
2220
2014
|
import path5 from "node:path";
|
|
2221
2015
|
|
|
2016
|
+
// scripts/init/project-profile.ts
|
|
2017
|
+
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync4 } from "node:fs";
|
|
2018
|
+
import path4 from "node:path";
|
|
2019
|
+
|
|
2020
|
+
// scripts/version/kit-descriptor.ts
|
|
2021
|
+
var KIT_PACKAGE_NAME = "@joshuafolkken/kit";
|
|
2022
|
+
|
|
2023
|
+
// scripts/init/project-profile.ts
|
|
2024
|
+
import { execaSync } from "execa";
|
|
2025
|
+
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".svelte-kit"]);
|
|
2026
|
+
var WEB_EXTENSIONS = /* @__PURE__ */ new Set([".html", ".css", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
2027
|
+
var TYPESCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx"]);
|
|
2028
|
+
var PROFILE_ARG_LENGTH = 2;
|
|
2029
|
+
var PROFILE_NAMES = {
|
|
2030
|
+
basic: "basic",
|
|
2031
|
+
full: "full",
|
|
2032
|
+
static: "basic",
|
|
2033
|
+
node: "full"
|
|
2034
|
+
};
|
|
2035
|
+
function parse_profile(value) {
|
|
2036
|
+
return typeof value === "string" && Object.hasOwn(PROFILE_NAMES, value) ? PROFILE_NAMES[value] : void 0;
|
|
2037
|
+
}
|
|
2038
|
+
var LEGACY_NAMES = { basic: "static", full: "node" };
|
|
2039
|
+
function with_legacy_profile_names(args) {
|
|
2040
|
+
return args.map((argument, index) => {
|
|
2041
|
+
const profile = args[index - 1] === "--profile" ? parse_profile(argument) : void 0;
|
|
2042
|
+
return profile === void 0 ? argument : LEGACY_NAMES[profile];
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
function is_profile_pair(args) {
|
|
2046
|
+
return args.length === PROFILE_ARG_LENGTH && args[0] === "--profile";
|
|
2047
|
+
}
|
|
2048
|
+
function requested_profile(args, usage = "josh init [--profile basic|full]") {
|
|
2049
|
+
if (args.length === 0) return void 0;
|
|
2050
|
+
if (!is_profile_pair(args)) throw new Error(`Usage: ${usage}`);
|
|
2051
|
+
const profile = parse_profile(args[1]);
|
|
2052
|
+
if (profile !== void 0) return profile;
|
|
2053
|
+
throw new Error("Profile must be basic or full");
|
|
2054
|
+
}
|
|
2055
|
+
function read_manifest(root) {
|
|
2056
|
+
const filename = path4.join(root, "package.json");
|
|
2057
|
+
if (!existsSync3(filename)) return void 0;
|
|
2058
|
+
const parsed = JSON.parse(readFileSync4(filename, "utf8"));
|
|
2059
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2060
|
+
throw new Error("package.json must contain an object");
|
|
2061
|
+
}
|
|
2062
|
+
return parsed;
|
|
2063
|
+
}
|
|
2064
|
+
function is_record(value) {
|
|
2065
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2066
|
+
}
|
|
2067
|
+
function recorded_profile(manifest) {
|
|
2068
|
+
const { josh } = manifest;
|
|
2069
|
+
return is_record(josh) ? parse_profile(josh["profile"]) : void 0;
|
|
2070
|
+
}
|
|
2071
|
+
function has_build_script(value) {
|
|
2072
|
+
return is_record(value) && ("build" in value || "dev" in value);
|
|
2073
|
+
}
|
|
2074
|
+
function has_project_dependencies(value) {
|
|
2075
|
+
if (!is_record(value)) return false;
|
|
2076
|
+
return Object.keys(value).some((name) => name !== KIT_PACKAGE_NAME);
|
|
2077
|
+
}
|
|
2078
|
+
function inferred_profile(manifest) {
|
|
2079
|
+
if (has_project_dependencies(manifest["dependencies"]) || has_project_dependencies(manifest["devDependencies"])) {
|
|
2080
|
+
return { profile: "full", reason: "package dependencies" };
|
|
2081
|
+
}
|
|
2082
|
+
if (has_build_script(manifest["scripts"])) {
|
|
2083
|
+
return { profile: "full", reason: "build or dev script" };
|
|
2084
|
+
}
|
|
2085
|
+
return { profile: "basic", reason: "metadata-only package.json" };
|
|
2086
|
+
}
|
|
2087
|
+
function resolve_profile(root, requested) {
|
|
2088
|
+
if (requested !== void 0) return { profile: requested, reason: "explicit --profile" };
|
|
2089
|
+
const manifest = read_manifest(root);
|
|
2090
|
+
if (manifest === void 0) return { profile: "basic", reason: "no package.json" };
|
|
2091
|
+
const profile = recorded_profile(manifest);
|
|
2092
|
+
if (profile !== void 0) return { profile, reason: "package.json josh.profile" };
|
|
2093
|
+
return inferred_profile(manifest);
|
|
2094
|
+
}
|
|
2095
|
+
function should_visit(entry) {
|
|
2096
|
+
return entry.isDirectory() && !IGNORED_DIRECTORIES.has(entry.name);
|
|
2097
|
+
}
|
|
2098
|
+
function scan_directory(directory, pending) {
|
|
2099
|
+
const entries = readdirSync(directory, { withFileTypes: true });
|
|
2100
|
+
pending.push(
|
|
2101
|
+
...entries.filter((entry) => should_visit(entry)).map((entry) => path4.join(directory, entry.name))
|
|
2102
|
+
);
|
|
2103
|
+
return entries.filter((entry) => entry.isFile()).map((entry) => path4.extname(entry.name));
|
|
2104
|
+
}
|
|
2105
|
+
function file_extensions(root) {
|
|
2106
|
+
const pending = [root];
|
|
2107
|
+
const extensions = [];
|
|
2108
|
+
for (const directory of pending) {
|
|
2109
|
+
extensions.push(...scan_directory(directory, pending));
|
|
2110
|
+
}
|
|
2111
|
+
return extensions;
|
|
2112
|
+
}
|
|
2113
|
+
function scan_files(root) {
|
|
2114
|
+
const extensions = file_extensions(root);
|
|
2115
|
+
const has_web = extensions.some((extension) => WEB_EXTENSIONS.has(extension));
|
|
2116
|
+
const has_typescript = extensions.some((extension) => TYPESCRIPT_EXTENSIONS.has(extension));
|
|
2117
|
+
return { has_web, has_typescript };
|
|
2118
|
+
}
|
|
2119
|
+
function has_github_remote(root) {
|
|
2120
|
+
const result = execaSync("git", ["config", "--get", "remote.origin.url"], {
|
|
2121
|
+
cwd: root,
|
|
2122
|
+
reject: false
|
|
2123
|
+
});
|
|
2124
|
+
return result.exitCode === 0 && repo_origin.parse_origin_url(result.stdout) !== void 0;
|
|
2125
|
+
}
|
|
2126
|
+
function inspect_project(root, requested) {
|
|
2127
|
+
const profile = resolve_profile(root, requested);
|
|
2128
|
+
const files = scan_files(root);
|
|
2129
|
+
const has_git = existsSync3(path4.join(root, ".git"));
|
|
2130
|
+
return { ...profile, ...files, has_git, has_github: has_git && has_github_remote(root) };
|
|
2131
|
+
}
|
|
2132
|
+
var project_profile = {
|
|
2133
|
+
parse_profile,
|
|
2134
|
+
resolve_profile,
|
|
2135
|
+
inspect_project,
|
|
2136
|
+
requested_profile,
|
|
2137
|
+
with_legacy_profile_names
|
|
2138
|
+
};
|
|
2139
|
+
|
|
2222
2140
|
// scripts/test/skip-marker.ts
|
|
2223
2141
|
var SKIP_MARKER = "\u2014 skipping";
|
|
2224
2142
|
|
|
@@ -2261,7 +2179,7 @@ var DEPENDENCY_FIELDS = ["dependencies", "devDependencies"];
|
|
|
2261
2179
|
var object_schema = z5.record(z5.string(), z5.unknown());
|
|
2262
2180
|
function project_root(directory) {
|
|
2263
2181
|
let current = directory;
|
|
2264
|
-
while (!
|
|
2182
|
+
while (!existsSync4(path5.join(current, PACKAGE_JSON))) {
|
|
2265
2183
|
const parent = path5.dirname(current);
|
|
2266
2184
|
if (parent === current) return directory;
|
|
2267
2185
|
current = parent;
|
|
@@ -2275,7 +2193,7 @@ function has_files(directory, pattern) {
|
|
|
2275
2193
|
return globSync2(pattern, { cwd: project_root(directory), exclude: is_ignored }).length > 0;
|
|
2276
2194
|
}
|
|
2277
2195
|
function has_config(directory, names) {
|
|
2278
|
-
return names.some((name) =>
|
|
2196
|
+
return names.some((name) => existsSync4(path5.join(project_root(directory), name)));
|
|
2279
2197
|
}
|
|
2280
2198
|
function has_bin(directory, bin_name) {
|
|
2281
2199
|
return find_local_bin_upwards(directory, bin_name) !== void 0;
|
|
@@ -2284,12 +2202,12 @@ function josh_settings(manifest) {
|
|
|
2284
2202
|
if (typeof manifest !== "object" || manifest === null || !("josh" in manifest)) return void 0;
|
|
2285
2203
|
return manifest.josh;
|
|
2286
2204
|
}
|
|
2287
|
-
function
|
|
2205
|
+
function is_basic_manifest(manifest) {
|
|
2288
2206
|
const josh = josh_settings(manifest);
|
|
2289
2207
|
if (typeof josh !== "object" || josh === null || !("profile" in josh)) return false;
|
|
2290
|
-
return josh.profile === "
|
|
2208
|
+
return project_profile.parse_profile(josh.profile) === "basic";
|
|
2291
2209
|
}
|
|
2292
|
-
function
|
|
2210
|
+
function read_manifest2(directory) {
|
|
2293
2211
|
try {
|
|
2294
2212
|
const manifest_path = path5.join(project_root(directory), PACKAGE_JSON);
|
|
2295
2213
|
return JSON.parse(readFileSync5(manifest_path, "utf8"));
|
|
@@ -2297,11 +2215,11 @@ function read_manifest(directory) {
|
|
|
2297
2215
|
return void 0;
|
|
2298
2216
|
}
|
|
2299
2217
|
}
|
|
2300
|
-
function
|
|
2301
|
-
return
|
|
2218
|
+
function is_basic(directory) {
|
|
2219
|
+
return is_basic_manifest(read_manifest2(directory));
|
|
2302
2220
|
}
|
|
2303
2221
|
function is_declared(directory, package_name) {
|
|
2304
|
-
const manifest = object_schema.safeParse(
|
|
2222
|
+
const manifest = object_schema.safeParse(read_manifest2(directory));
|
|
2305
2223
|
if (!manifest.success) return false;
|
|
2306
2224
|
return DEPENDENCY_FIELDS.some((field) => {
|
|
2307
2225
|
const dependencies = object_schema.safeParse(manifest.data[field]);
|
|
@@ -2316,18 +2234,18 @@ function skip_notice(check3, reason) {
|
|
|
2316
2234
|
return `josh ${check3}: ${reason} ${SKIP_MARKER} ${check3}.`;
|
|
2317
2235
|
}
|
|
2318
2236
|
function type_check_skip_reason(directory) {
|
|
2319
|
-
if (!
|
|
2237
|
+
if (!is_basic(directory)) return void 0;
|
|
2320
2238
|
if (!has_files(directory, TYPE_FILES)) return "no TypeScript files were found";
|
|
2321
2239
|
if (!has_config(directory, TYPE_CONFIGS)) return "no TypeScript configuration was found";
|
|
2322
2240
|
return missing_tool_reason(directory, "typescript", "tsc");
|
|
2323
2241
|
}
|
|
2324
2242
|
function prettier_skip_reason(directory) {
|
|
2325
|
-
if (!
|
|
2243
|
+
if (!is_basic(directory)) return void 0;
|
|
2326
2244
|
if (!has_files(directory, WEB_FILES)) return "no HTML, CSS or JavaScript files were found";
|
|
2327
2245
|
return missing_tool_reason(directory, "prettier", "prettier");
|
|
2328
2246
|
}
|
|
2329
2247
|
function eslint_skip_reason(directory) {
|
|
2330
|
-
if (!
|
|
2248
|
+
if (!is_basic(directory)) return void 0;
|
|
2331
2249
|
if (!has_files(directory, SCRIPT_FILES)) return "no JavaScript or TypeScript files were found";
|
|
2332
2250
|
if (!has_config(directory, ESLINT_CONFIGS)) return "no ESLint configuration was found";
|
|
2333
2251
|
return missing_tool_reason(directory, "eslint", "eslint");
|
|
@@ -2343,7 +2261,7 @@ var project_checks = {
|
|
|
2343
2261
|
has_bin,
|
|
2344
2262
|
has_config,
|
|
2345
2263
|
has_files,
|
|
2346
|
-
|
|
2264
|
+
is_basic,
|
|
2347
2265
|
prettier_skip_reason,
|
|
2348
2266
|
project_root,
|
|
2349
2267
|
skip_notice,
|
|
@@ -2355,34 +2273,59 @@ var TEST_SUFFIXES = [".e2e.ts", ".test.ts"];
|
|
|
2355
2273
|
var EXEMPT_PATHS = [".editorconfig"];
|
|
2356
2274
|
var EXEMPT_PREFIXES = [".idea/", ".vscode/", "prompts/"];
|
|
2357
2275
|
var EXEMPT_SUFFIXES = [".md"];
|
|
2358
|
-
var
|
|
2276
|
+
var BASIC_VISUAL_SUFFIXES = [".html", ".css"];
|
|
2277
|
+
var BASIC_REQUIRED_SUFFIXES = [
|
|
2278
|
+
".ts",
|
|
2279
|
+
".tsx",
|
|
2280
|
+
".mts",
|
|
2281
|
+
".cts",
|
|
2282
|
+
".js",
|
|
2283
|
+
".jsx",
|
|
2284
|
+
".mjs",
|
|
2285
|
+
".cjs",
|
|
2286
|
+
".svelte",
|
|
2287
|
+
".json",
|
|
2288
|
+
".jsonc",
|
|
2289
|
+
".json5",
|
|
2290
|
+
".yaml",
|
|
2291
|
+
".yml",
|
|
2292
|
+
".toml"
|
|
2293
|
+
];
|
|
2359
2294
|
function normalize(paths) {
|
|
2360
2295
|
return paths.map((path7) => path7.trim()).filter((path7) => path7 !== "");
|
|
2361
2296
|
}
|
|
2362
2297
|
function is_test_file(path7) {
|
|
2363
2298
|
return TEST_SUFFIXES.some((suffix) => path7.endsWith(suffix));
|
|
2364
2299
|
}
|
|
2365
|
-
function
|
|
2366
|
-
|
|
2300
|
+
function is_basic_manual(path7, is_basic2) {
|
|
2301
|
+
const lower = path7.toLowerCase();
|
|
2302
|
+
return is_basic2 && BASIC_REQUIRED_SUFFIXES.every((suffix) => !lower.endsWith(suffix));
|
|
2303
|
+
}
|
|
2304
|
+
function is_basic_visual(path7) {
|
|
2305
|
+
const lower = path7.toLowerCase();
|
|
2306
|
+
return BASIC_VISUAL_SUFFIXES.some((suffix) => lower.endsWith(suffix));
|
|
2367
2307
|
}
|
|
2368
|
-
function is_exempt(path7,
|
|
2369
|
-
if (EXEMPT_PATHS.includes(path7) ||
|
|
2308
|
+
function is_exempt(path7, is_basic2 = false) {
|
|
2309
|
+
if (EXEMPT_PATHS.includes(path7) || is_basic_manual(path7, is_basic2)) return true;
|
|
2370
2310
|
return EXEMPT_SUFFIXES.some((suffix) => path7.endsWith(suffix)) || EXEMPT_PREFIXES.some((prefix) => path7.startsWith(prefix));
|
|
2371
2311
|
}
|
|
2372
|
-
function is_runtime(path7,
|
|
2373
|
-
return !is_test_file(path7) && !is_exempt(path7,
|
|
2312
|
+
function is_runtime(path7, is_basic2 = false) {
|
|
2313
|
+
return !is_test_file(path7) && !is_exempt(path7, is_basic2);
|
|
2374
2314
|
}
|
|
2375
|
-
function verdict_for(paths,
|
|
2315
|
+
function verdict_for(paths, is_basic2 = false) {
|
|
2376
2316
|
const changed = normalize(paths);
|
|
2377
2317
|
if (changed.some((path7) => is_test_file(path7))) return "satisfied";
|
|
2378
|
-
if (changed.some((path7) => is_runtime(path7,
|
|
2318
|
+
if (changed.some((path7) => is_runtime(path7, is_basic2))) return "required";
|
|
2379
2319
|
return "exempt";
|
|
2380
2320
|
}
|
|
2381
|
-
function runtime_files(paths,
|
|
2382
|
-
return normalize(paths).filter((path7) => is_runtime(path7,
|
|
2321
|
+
function runtime_files(paths, is_basic2 = false) {
|
|
2322
|
+
return normalize(paths).filter((path7) => is_runtime(path7, is_basic2));
|
|
2323
|
+
}
|
|
2324
|
+
function exempt_files(paths, is_basic2 = false) {
|
|
2325
|
+
return normalize(paths).filter((path7) => is_exempt(path7, is_basic2));
|
|
2383
2326
|
}
|
|
2384
|
-
function
|
|
2385
|
-
return normalize(paths).filter((path7) => is_exempt(path7,
|
|
2327
|
+
function manual_check_files(paths, is_basic2) {
|
|
2328
|
+
return normalize(paths).filter((path7) => is_exempt(path7, is_basic2) && !is_exempt(path7));
|
|
2386
2329
|
}
|
|
2387
2330
|
var test_declared_logic = {
|
|
2388
2331
|
EXEMPT_PATHS,
|
|
@@ -2390,9 +2333,11 @@ var test_declared_logic = {
|
|
|
2390
2333
|
EXEMPT_SUFFIXES,
|
|
2391
2334
|
TEST_SUFFIXES,
|
|
2392
2335
|
exempt_files,
|
|
2336
|
+
is_basic_visual,
|
|
2393
2337
|
is_exempt,
|
|
2394
2338
|
is_runtime,
|
|
2395
2339
|
is_test_file,
|
|
2340
|
+
manual_check_files,
|
|
2396
2341
|
runtime_files,
|
|
2397
2342
|
verdict_for
|
|
2398
2343
|
};
|
|
@@ -2421,7 +2366,7 @@ function source_paths() {
|
|
|
2421
2366
|
return injected.paths ?? read_changed_paths_sync();
|
|
2422
2367
|
}
|
|
2423
2368
|
function current_verdict(paths = source_paths()) {
|
|
2424
|
-
return test_declared_logic.verdict_for(paths, project_checks.
|
|
2369
|
+
return test_declared_logic.verdict_for(paths, project_checks.is_basic(process.cwd()));
|
|
2425
2370
|
}
|
|
2426
2371
|
function restore_injection(previous) {
|
|
2427
2372
|
if (previous === void 0) {
|
|
@@ -2447,8 +2392,8 @@ function is_untested_commit(call) {
|
|
|
2447
2392
|
return test_declared_changed.current_verdict() === "required";
|
|
2448
2393
|
}
|
|
2449
2394
|
var REASON = '\u26D4 test declared: this commits a runtime code change with no test beside it \u2014 `pnpm josh test:declared` reads the working tree as `required` (a changed file that is neither a test \u2014 `*.test.ts` / `*.e2e.ts` \u2014 nor a mechanically-exempt non-runtime path). `CLAUDE.md` \u2192 Code Change Rules requires a test for ALL code changes: a bug fix gets a regression test, a refactor gets tests pinning behavior first, logic gets a unit test, an observable UI/timing change gets an E2E. Add the declared test and reissue this commit. **If the change genuinely touches no executable runtime path** it is the pre-approved non-runtime exception \u2014 declare it in your Step 0 work summary with the reason it is exempt (non-executable config, a cosmetic asset swap) and reissue; the exception is a presentation, not a stop. Run `pnpm josh test:declared` to see the files it names. The procedure is `prompts/collaboration-workflow/rule-delivery.md` \u2192 "\u914D\u9001\u3055\u308C\u3066\u3044\u308B\u898F\u5247". This fires once per run, so reissuing will not repeat on the call in hand.';
|
|
2450
|
-
var
|
|
2451
|
-
var test_declared_commit = { REASON, ROW:
|
|
2395
|
+
var ROW13 = { id: "test-declared", is_trigger: is_untested_commit, reason: REASON };
|
|
2396
|
+
var test_declared_commit = { REASON, ROW: ROW13, is_untested_commit };
|
|
2452
2397
|
|
|
2453
2398
|
// scripts/rules/third-party-write.ts
|
|
2454
2399
|
function is_third_party_write_segment(segment, session_owner) {
|
|
@@ -2464,14 +2409,14 @@ function writes_third_party(command, resolve_owner = repo_party.current_owner) {
|
|
|
2464
2409
|
return targets.some((segment) => is_third_party_write_segment(segment, session_owner));
|
|
2465
2410
|
}
|
|
2466
2411
|
var THIRD_PARTY_WRITE_REASON = '\u26D4 third-party write: this `gh api` call writes to a repository whose owner is not this session \u2014 a tracker we do not own is Tier C, and every write there (Issue, comment, PR, Discussion, review) needs explicit current-turn user instruction; typing the target, or holding a correct diagnosis, is not that instruction (`CLAUDE.md` \u2192 "Third-party repositories are Tier C", `prompts/collaboration-workflow/upstream-interrupt.md`). Confirm the party with `pnpm josh repo:party <owner/repo>` \u2014 owner equality, computed not judged. A read (`-X GET`, or no field flag) passes untouched; only a write (`-f`/`-F`/`--field`/`--raw-field`/`--input`, or `-X POST|PATCH|PUT|DELETE`) is refused. Record the finding on our own Issue and draft the artifact for a person to post \u2014 do not publish it yourself. **This rule fires on every occurrence, not once per run.**';
|
|
2467
|
-
var
|
|
2412
|
+
var ROW14 = {
|
|
2468
2413
|
id: "third-party-write",
|
|
2469
2414
|
is_trigger: bash_triggers.on_bash_command((command) => writes_third_party(command)),
|
|
2470
2415
|
reason: THIRD_PARTY_WRITE_REASON,
|
|
2471
2416
|
decide: () => true
|
|
2472
2417
|
};
|
|
2473
2418
|
var third_party_write = {
|
|
2474
|
-
ROW:
|
|
2419
|
+
ROW: ROW14,
|
|
2475
2420
|
THIRD_PARTY_WRITE_REASON,
|
|
2476
2421
|
is_third_party_write_segment,
|
|
2477
2422
|
writes_third_party
|
|
@@ -2525,14 +2470,14 @@ function is_unauthorized_worktree_change(command) {
|
|
|
2525
2470
|
return shell_segments.segments_of(command).some((segment) => is_unauthorized_segment(segment));
|
|
2526
2471
|
}
|
|
2527
2472
|
var WORKTREE_MUTATION_REASON = '\u26D4 unauthorized working-tree change: `git checkout -- <path>`, `git restore <path>` and an unauthorized `git stash` mutate the working tree or the repository-wide stash every work tree shares, and none is on the `.claude/settings.json` deny list (`operating-rules.md`). Do not run them on your own judgement. A commit goes through `pnpm josh git`; a stash pop goes through `pnpm josh stash:pop "<message>"`, never a positional `git stash pop` a shared stack lets another lane divert. A stash push is authorized only inside a documented flow and only with a findable message \u2014 `git stash push -u -m "<message>"` \u2014 which this row allows; a bare `git stash`, a message-less push, or `pop` / `apply` / `drop` is refused. **This rule fires on every occurrence, not once per run.**';
|
|
2528
|
-
var
|
|
2473
|
+
var ROW15 = {
|
|
2529
2474
|
id: "worktree-mutation",
|
|
2530
2475
|
is_trigger: bash_triggers.on_bash_command(is_unauthorized_worktree_change),
|
|
2531
2476
|
reason: WORKTREE_MUTATION_REASON,
|
|
2532
2477
|
decide: () => true
|
|
2533
2478
|
};
|
|
2534
2479
|
var worktree_guard = {
|
|
2535
|
-
ROW:
|
|
2480
|
+
ROW: ROW15,
|
|
2536
2481
|
WORKTREE_MUTATION_REASON,
|
|
2537
2482
|
is_unauthorized_stash,
|
|
2538
2483
|
is_unauthorized_worktree_change,
|
|
@@ -2569,7 +2514,7 @@ function is_body_only_issue_read(command) {
|
|
|
2569
2514
|
if (segments.some((segment) => fetches_issue_comments(segment))) return false;
|
|
2570
2515
|
return segments.some((segment) => is_body_read_segment(segment));
|
|
2571
2516
|
}
|
|
2572
|
-
var ISSUE_COMMENTS_REASON = "\u26D4 an Issue's comments are part of the Issue: read them before implementing, not only the body. A decision recorded after the body was written lives only in a comment \u2014 a corrected diagnosis (joshuafolkken/kit#1537), a changed default and an added acceptance criterion (joshuafolkken/kit#1520), a scope handed to another Issue (joshuafolkken/kit#1304) \u2014 and nothing in the body says it was superseded, so a body-only reader builds the wrong thing and sees no contradiction. Reissue this read with the comments included: `gh api repos/{owner}/{repo}/issues/<N>/comments --jq '.[] | {user: .user.login, created_at, body}'` beside the body read, or `gh issue view <N> --comments` where GraphQL is reachable. Then the later text is the agreement in force \u2014 a comment supersedes the body it contradicts \u2014 except for two answers that are not the run's to make: work a comment reassigns to another Issue is out of scope and is not implemented, and a comment saying the Issue no longer has a reason to exist stops the run with a `confirmation` Telegram. The procedure is `.claude/skills/workflow-commands/SKILL.md` \u2192 \"An Issue's comments are part of the Issue\".
|
|
2517
|
+
var ISSUE_COMMENTS_REASON = "\u26D4 an Issue's comments are part of the Issue: read them before implementing, not only the body. A decision recorded after the body was written lives only in a comment \u2014 a corrected diagnosis (joshuafolkken/kit#1537), a changed default and an added acceptance criterion (joshuafolkken/kit#1520), a scope handed to another Issue (joshuafolkken/kit#1304) \u2014 and nothing in the body says it was superseded, so a body-only reader builds the wrong thing and sees no contradiction. Reissue this read with the comments included: `gh api repos/{owner}/{repo}/issues/<N>/comments --jq '.[] | {user: .user.login, created_at, body}'` beside the body read, or `gh issue view <N> --comments` where GraphQL is reachable. Then the later text is the agreement in force \u2014 a comment supersedes the body it contradicts \u2014 except for two answers that are not the run's to make: work a comment reassigns to another Issue is out of scope and is not implemented, and a comment saying the Issue no longer has a reason to exist stops the run with a `confirmation` Telegram. The procedure is `.claude/skills/workflow-commands/SKILL.md` \u2192 \"An Issue's comments are part of the Issue\". Every body-only read is refused until the comments are read.";
|
|
2573
2518
|
var { SHELL_BODY_REASON: SHELL_BODY_REASON2, carries_a_body: carries_a_body2, is_shell_evaluated_body: is_shell_evaluated_body2, keeps_body_safe: keeps_body_safe2 } = shell_body_trigger;
|
|
2574
2519
|
var ISSUE_LISTING = /^gh\s+(?:-{1,2}[\w-]+(?:[= ]\S+)?\s+)*issue\s+list\b/u;
|
|
2575
2520
|
var ISSUES_QUERY = /repos\/[^\s'"]*\/issues\?[^\s'"]*state=open/u;
|
|
@@ -2593,26 +2538,24 @@ var DELIVERED_RULES = [
|
|
|
2593
2538
|
// before any of those speak; on a first-party filing it is silent (the owners match), so `wip-cap`
|
|
2594
2539
|
// still speaks first, exactly as before. It fires on every occurrence (`git-force.ts`).
|
|
2595
2540
|
third_party_write.ROW,
|
|
2541
|
+
// **A hand-built filing is refused on every occurrence** (joshuafolkken/kit#2808) and pointed at
|
|
2542
|
+
// `josh issue:file`, which runs the scout, the lint, the labels, the `## Origin` check and
|
|
2543
|
+
// `epic:bundle` itself. It claims the direct call only, so the rows below — which trigger on the
|
|
2544
|
+
// `issue:file` call — never overlap it.
|
|
2545
|
+
direct_filing.ROW,
|
|
2596
2546
|
{
|
|
2597
2547
|
id: "wip-cap",
|
|
2598
2548
|
is_trigger: on_bash_command(is_issue_filing),
|
|
2599
2549
|
reason: WIP_CAP_REASON,
|
|
2600
2550
|
keeps: on_bash_command(counts_open_issues)
|
|
2601
2551
|
},
|
|
2602
|
-
// **
|
|
2603
|
-
// backlog count still speaks first: `
|
|
2604
|
-
// run,
|
|
2605
|
-
//
|
|
2606
|
-
//
|
|
2607
|
-
//
|
|
2608
|
-
issue_scout.ROW,
|
|
2552
|
+
// **Two more rows share the filing trigger** (joshuafolkken/kit#2119, joshuafolkken/kit#2213), listed
|
|
2553
|
+
// after `wip-cap` so the backlog count still speaks first: `filing-cap` refuses every filing past
|
|
2554
|
+
// the per-run ceiling, and `issue-fold` refuses a *second* filing the run has not folded. Each is a
|
|
2555
|
+
// real instance of the admissible overlap the comment on `delivery` describes — the losing rule's
|
|
2556
|
+
// delivery is still correct one reissue later — so an unfolded, over-cap second filing is delivered
|
|
2557
|
+
// `wip-cap`, then `filing-cap`, then `issue-fold` across its reissues.
|
|
2609
2558
|
filing_cap.ROW,
|
|
2610
|
-
// **`issue-fold` shares the same filing trigger** (joshuafolkken/kit#2213), listed after
|
|
2611
|
-
// `filing-cap`: it refuses a *second* filing the run has not folded, once per run, stood down off
|
|
2612
|
-
// the tail when the run has already folded or has filed nothing earlier. It is another instance of
|
|
2613
|
-
// the admissible overlap — the losing rule's delivery is still correct one reissue later — so a
|
|
2614
|
-
// scout-less, unfolded, over-cap second filing is delivered `wip-cap`, then `issue-scout`, then
|
|
2615
|
-
// `filing-cap`, then `issue-fold` across its reissues.
|
|
2616
2559
|
issue_fold.ROW,
|
|
2617
2560
|
{
|
|
2618
2561
|
id: "issue-comments",
|
|
@@ -2839,13 +2782,9 @@ var DELIVERED_RULES = [
|
|
|
2839
2782
|
rule_body_guard.ROW,
|
|
2840
2783
|
// **The generic oracle-consulted rows, one per oracle that declared a firing point**
|
|
2841
2784
|
// (joshuafolkken/kit#2324). Each refuses the action the oracle governs when the run has not run that
|
|
2842
|
-
// oracle's command first, stood down once it has
|
|
2843
|
-
//
|
|
2844
|
-
|
|
2845
|
-
// `issue-fold` have each had their turn. The non-overlapping firing point (`pkg:scout` on a package
|
|
2846
|
-
// add) claims a command no row above matches.
|
|
2847
|
-
...oracle_consulted.ROWS,
|
|
2848
|
-
issue_bug_label_rule.ROW
|
|
2785
|
+
// oracle's command first, stood down once it has. Listed last; the one firing point left
|
|
2786
|
+
// (`pkg:scout` on a package add) claims a command no row above matches.
|
|
2787
|
+
...oracle_consulted.ROWS
|
|
2849
2788
|
];
|
|
2850
2789
|
var ALONE_IN_TURN = 1;
|
|
2851
2790
|
function batches_the_turn(call, turn) {
|
|
@@ -2879,12 +2818,12 @@ function is_first_delivery(tail, call, delivered_at_ms, run2) {
|
|
|
2879
2818
|
return delivered_at_ms === hook_decision.NEVER_MS && !will_batch_guard_refuse(tail, call, run2);
|
|
2880
2819
|
}
|
|
2881
2820
|
function delivery_decision(rule) {
|
|
2882
|
-
const { decide:
|
|
2883
|
-
if (
|
|
2821
|
+
const { decide: decide5, already_satisfied } = rule;
|
|
2822
|
+
if (decide5 === void 0 && already_satisfied === void 0) return is_first_delivery;
|
|
2884
2823
|
return function should_block(tail, call, delivered_at_ms, run2) {
|
|
2885
2824
|
if (already_satisfied?.(tail, call) === true) return false;
|
|
2886
|
-
if (
|
|
2887
|
-
return
|
|
2825
|
+
if (decide5 === void 0) return true;
|
|
2826
|
+
return decide5(call, run2, !will_batch_guard_refuse(tail, call, run2), delivered_at_ms);
|
|
2888
2827
|
};
|
|
2889
2828
|
}
|
|
2890
2829
|
function guard_of(rule) {
|
|
@@ -2914,11 +2853,11 @@ var delivered_rules = {
|
|
|
2914
2853
|
DELIVERED_RULES,
|
|
2915
2854
|
EARLY_HEARTBEAT_REASON: early_heartbeat.EARLY_HEARTBEAT_REASON,
|
|
2916
2855
|
FILE_BODY_REASON: file_body.FILE_BODY_REASON,
|
|
2856
|
+
DIRECT_FILING_REASON: direct_filing.DIRECT_FILING_REASON,
|
|
2917
2857
|
FILING_CAP_REASON: filing_cap.FILING_CAP_REASON,
|
|
2918
2858
|
GIT_FORCE_REASON: git_force.GIT_FORCE_REASON,
|
|
2919
2859
|
ISSUE_COMMENTS_REASON,
|
|
2920
2860
|
ISSUE_FOLD_REASON: issue_fold.ISSUE_FOLD_REASON,
|
|
2921
|
-
ISSUE_SCOUT_REASON: issue_scout.ISSUE_SCOUT_REASON,
|
|
2922
2861
|
LANE_INTERACTIVE_ASK_REASON: lane_interactive_ask.LANE_INTERACTIVE_ASK_REASON,
|
|
2923
2862
|
LANE_PARK_REASON: lane_park.LANE_PARK_REASON,
|
|
2924
2863
|
MEASURED_RULES,
|
|
@@ -3590,7 +3529,7 @@ var git_gh_issue = {
|
|
|
3590
3529
|
// scripts/ci/pr-classification.ts
|
|
3591
3530
|
import { readFileSync as readFileSync6 } from "node:fs";
|
|
3592
3531
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3593
|
-
import { execaSync } from "execa";
|
|
3532
|
+
import { execaSync as execaSync2 } from "execa";
|
|
3594
3533
|
import { z as z8 } from "zod";
|
|
3595
3534
|
var CLASSIFICATION_SET = new Set(RELEASE_CLASSIFICATION_NAMES);
|
|
3596
3535
|
var ISSUE_BUG_LABEL = "bug";
|
|
@@ -3644,7 +3583,7 @@ function select_issue_classification(issue_json) {
|
|
|
3644
3583
|
return label;
|
|
3645
3584
|
}
|
|
3646
3585
|
function fetch_current_labels(repository, pull_number) {
|
|
3647
|
-
const result =
|
|
3586
|
+
const result = execaSync2(
|
|
3648
3587
|
"gh",
|
|
3649
3588
|
["api", `repos/${repository}/issues/${String(pull_number)}/labels`, "--jq", ".[].name"],
|
|
3650
3589
|
{ reject: false }
|
|
@@ -4984,7 +4923,7 @@ function to_advice(verdict, has_work_to_stash) {
|
|
|
4984
4923
|
if (verdict === UNDETERMINED_VERDICT) return UNDETERMINED_ADVICE;
|
|
4985
4924
|
return has_work_to_stash ? STOPPED_DIRTY_ADVICE : STOPPED_CLEAN_ADVICE;
|
|
4986
4925
|
}
|
|
4987
|
-
function
|
|
4926
|
+
function decide3(traces) {
|
|
4988
4927
|
const verdict = to_verdict(traces);
|
|
4989
4928
|
const has_work_to_stash = verdict === STOPPED_VERDICT && traces.is_tree_dirty;
|
|
4990
4929
|
return {
|
|
@@ -5083,11 +5022,11 @@ async function read_traces(request) {
|
|
|
5083
5022
|
}
|
|
5084
5023
|
async function check(request) {
|
|
5085
5024
|
run_issue_number.require_issue_number(request.issue);
|
|
5086
|
-
return
|
|
5025
|
+
return decide3(await read_traces(request));
|
|
5087
5026
|
}
|
|
5088
5027
|
var run_liveness = {
|
|
5089
5028
|
check,
|
|
5090
|
-
decide:
|
|
5029
|
+
decide: decide3,
|
|
5091
5030
|
describe_agent_state,
|
|
5092
5031
|
read_child_settled,
|
|
5093
5032
|
read_agent_state,
|
|
@@ -5237,7 +5176,7 @@ function to_advice2(verdict, issue, tree) {
|
|
|
5237
5176
|
if (verdict === PARK_VERDICT) return PARK_ADVICE;
|
|
5238
5177
|
return CLEAN_ADVICE;
|
|
5239
5178
|
}
|
|
5240
|
-
function
|
|
5179
|
+
function decide4(tree, child, issue) {
|
|
5241
5180
|
const verdict = to_verdict2(tree, child);
|
|
5242
5181
|
return {
|
|
5243
5182
|
advice: to_advice2(verdict, issue, tree),
|
|
@@ -5306,8 +5245,8 @@ async function read_child_state(issue) {
|
|
|
5306
5245
|
async function check2(issue) {
|
|
5307
5246
|
require_issue_number(issue);
|
|
5308
5247
|
const tree = await read_tree_state();
|
|
5309
|
-
if (needs_reclaim(tree)) return
|
|
5310
|
-
return
|
|
5248
|
+
if (needs_reclaim(tree)) return decide4(tree, NO_CHILD_WORK, issue);
|
|
5249
|
+
return decide4(tree, await read_child_state(issue), issue);
|
|
5311
5250
|
}
|
|
5312
5251
|
var run_preflight = {
|
|
5313
5252
|
CLEAN_ADVICE,
|
|
@@ -5322,7 +5261,7 @@ var run_preflight = {
|
|
|
5322
5261
|
MERGED_PR,
|
|
5323
5262
|
STASH_LABEL_PREFIX,
|
|
5324
5263
|
check: check2,
|
|
5325
|
-
decide:
|
|
5264
|
+
decide: decide4,
|
|
5326
5265
|
read_child_state,
|
|
5327
5266
|
read_pr_state: read_pr_state2,
|
|
5328
5267
|
to_pr_state: to_pr_state2
|