@joshuafolkken/kit 1.953.0 → 1.957.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/README.md +2 -0
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-V7RH56HU.js → chunk-244WYOEO.js} +2 -2
- package/dist/hooks/{chunk-IA7WC4NS.js → chunk-3ON7HOYB.js} +1 -1
- package/dist/hooks/{chunk-EYFJ74MJ.js → chunk-BELI5XSS.js} +6 -2
- package/dist/hooks/{chunk-IBFC25IX.js → chunk-GNOITLRR.js} +4 -4
- package/dist/hooks/{chunk-M4U4YNRE.js → chunk-HJXQZ7V4.js} +3 -3
- package/dist/hooks/{chunk-452OKRQX.js → chunk-IJPBTDTB.js} +63 -2
- package/dist/hooks/{chunk-NKVLBB65.js → chunk-KOCK5FXQ.js} +3 -3
- package/dist/hooks/{chunk-O3PEDF3M.js → chunk-LDQDZYHX.js} +1 -1
- package/dist/hooks/{chunk-Z5TVZ4AP.js → chunk-M6K7EQDO.js} +8 -8
- package/dist/hooks/{chunk-OM5VDM6F.js → chunk-XSL4SRMA.js} +2 -2
- 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 +3 -3
- package/dist/josh.js +6 -2
- package/package.json +1 -1
- package/scripts/backlog/backlog-drive-cli.ts +5 -1
- package/scripts/backlog/backlog-drive-retrospective.ts +20 -0
- package/scripts/backlog/backlog-drive.ts +7 -3
- package/scripts/git/issue-labels.ts +19 -0
- package/scripts/init/project-profile.ts +9 -5
- package/scripts/init/start-plan.ts +145 -0
- package/scripts/init/start-prompt.ts +55 -0
- package/scripts/init/start-steps.ts +167 -0
- package/scripts/init/start.ts +92 -16
- package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +2 -1
- package/scripts/josh/josh-commands-project.ts +6 -2
- package/scripts/run/run-retrospective.ts +34 -0
- package/scripts/run/run-step-cli.ts +3 -5
- package/scripts/run/run-step.ts +3 -15
package/README.md
CHANGED
|
@@ -26,6 +26,8 @@ pnpm josh format
|
|
|
26
26
|
pnpm josh gate
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
`josh init` is the entry without GitHub. To start a new project on the GitHub Issue workflow (`kickoff`), run `pnpm exec josh start` instead of `josh init` — it also creates the Git repository, the GitHub repository and the labels ([init.md → `josh init` or `josh start`](./docs/init.md#josh-init-or-josh-start)).
|
|
30
|
+
|
|
29
31
|
It needs [Node.js](https://nodejs.org/) 22.19.0 or later with [pnpm](https://pnpm.io/), even for a `static` project; [getting-started.md](./docs/getting-started.md) installs both. The global `josh` command ([cli.md](./docs/cli.md)) and the [gh CLI](https://cli.github.com/) for `josh version` and the GitHub Issue workflow are optional.
|
|
30
32
|
|
|
31
33
|
## Documentation
|
|
@@ -9,10 +9,10 @@ import {
|
|
|
9
9
|
load_environment_file,
|
|
10
10
|
notice_refusal_path,
|
|
11
11
|
refusal_path
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
12
|
+
} from "./chunk-244WYOEO.js";
|
|
13
|
+
import "./chunk-XSL4SRMA.js";
|
|
14
|
+
import "./chunk-LDQDZYHX.js";
|
|
15
|
+
import "./chunk-BELI5XSS.js";
|
|
16
16
|
import "./chunk-WPALEIRX.js";
|
|
17
17
|
import "./chunk-RKUAD3I6.js";
|
|
18
18
|
export {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
lane_guard_policy,
|
|
3
3
|
time_batch_guard
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-XSL4SRMA.js";
|
|
5
5
|
import {
|
|
6
6
|
hook_decision
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-LDQDZYHX.js";
|
|
8
8
|
|
|
9
9
|
// scripts/hooks/batch-guard.ts
|
|
10
10
|
import { text } from "node:stream/consumers";
|
|
@@ -1016,9 +1016,13 @@ var PROJECT_COMMANDS = {
|
|
|
1016
1016
|
},
|
|
1017
1017
|
start: {
|
|
1018
1018
|
script: "scripts/init/start.ts",
|
|
1019
|
-
description: "
|
|
1019
|
+
description: "Create a new project for the GitHub Issue workflow, from git init to labels",
|
|
1020
1020
|
category: "Project",
|
|
1021
|
-
reference: [
|
|
1021
|
+
reference: [
|
|
1022
|
+
`${PROFILE_ARGUMENTS} [--yes] [--github] [--public]`,
|
|
1023
|
+
"developer",
|
|
1024
|
+
["files", "git", "network", "processes"]
|
|
1025
|
+
]
|
|
1022
1026
|
},
|
|
1023
1027
|
init: {
|
|
1024
1028
|
script: "scripts/init/init.ts",
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
investigation_reads
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-HJXQZ7V4.js";
|
|
4
4
|
import {
|
|
5
5
|
lane_guard_policy
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-XSL4SRMA.js";
|
|
7
7
|
import {
|
|
8
8
|
hook_decision
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-LDQDZYHX.js";
|
|
10
10
|
import {
|
|
11
11
|
json_value,
|
|
12
12
|
time_spans
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-BELI5XSS.js";
|
|
14
14
|
|
|
15
15
|
// scripts/delegation/duplicate-read-guard.ts
|
|
16
16
|
import { statSync } from "node:fs";
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
time_batch_guard
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-XSL4SRMA.js";
|
|
4
4
|
import {
|
|
5
5
|
hook_decision
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-LDQDZYHX.js";
|
|
7
7
|
import {
|
|
8
8
|
cost_blocks,
|
|
9
9
|
cost_transcript,
|
|
10
10
|
time_bundle_call,
|
|
11
11
|
time_shell,
|
|
12
12
|
time_spans
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-BELI5XSS.js";
|
|
14
14
|
|
|
15
15
|
// scripts/delegation/investigation-reads.ts
|
|
16
16
|
import path from "node:path";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
lane_child_marker,
|
|
3
3
|
run_issue_number
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-LDQDZYHX.js";
|
|
5
5
|
import {
|
|
6
6
|
PLATFORM_TEMP_ROOT,
|
|
7
7
|
cost_blocks,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
time_density_hook,
|
|
13
13
|
time_shell,
|
|
14
14
|
time_transcript_line
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-BELI5XSS.js";
|
|
16
16
|
import {
|
|
17
17
|
ENV_FILE_NAME,
|
|
18
18
|
LANE_SEAT_KEY,
|
|
@@ -1625,6 +1625,7 @@ var lane_background = {
|
|
|
1625
1625
|
};
|
|
1626
1626
|
|
|
1627
1627
|
// scripts/git/issue-labels.ts
|
|
1628
|
+
var EPIC_LABEL = "epic";
|
|
1628
1629
|
var BUG_LABEL = "bug";
|
|
1629
1630
|
var ENHANCEMENT_LABEL = "enhancement";
|
|
1630
1631
|
var BREAKING_CHANGE_LABEL = "breaking-change";
|
|
@@ -1632,6 +1633,66 @@ var IN_PROGRESS_LABEL = "in-progress";
|
|
|
1632
1633
|
var NEEDS_DECISION_LABEL = "needs-decision";
|
|
1633
1634
|
var NEEDS_HUMAN_REVIEW_LABEL = "needs-human-review";
|
|
1634
1635
|
var ALREADY_DONE_LABEL = "already-done";
|
|
1636
|
+
var INTERRUPT_ROUTE_LABEL = "route:interrupt";
|
|
1637
|
+
var REVIEW_CAP_ROUTE_LABEL = "route:review-cap";
|
|
1638
|
+
var SPLIT_ROUTE_LABEL = "route:split";
|
|
1639
|
+
var TIER_A_ROUTE_LABEL = "route:tier-a";
|
|
1640
|
+
var FILING_ROUTE_LABELS = [
|
|
1641
|
+
{
|
|
1642
|
+
name: INTERRUPT_ROUTE_LABEL,
|
|
1643
|
+
// Not `5319e7`, which is the `epic` label's purple: in a listing an interrupt would render as
|
|
1644
|
+
// an epic, and the two are read at a glance rather than by name.
|
|
1645
|
+
color: "1d76db",
|
|
1646
|
+
// Keyed to the tests rather than to severity. "A serious defect" is the self-assessment
|
|
1647
|
+
// `wip-cap.md` bans as a criterion, and this string is what a person reads in `gh label list`.
|
|
1648
|
+
description: "Filed past the WIP cap \u2014 meets one of the three interrupt tests (wip-cap.md)"
|
|
1649
|
+
},
|
|
1650
|
+
{
|
|
1651
|
+
name: REVIEW_CAP_ROUTE_LABEL,
|
|
1652
|
+
color: "eab308",
|
|
1653
|
+
description: "Filed by the review round cap's carry-forward (prompts/review.md \u2192 Review round cap)"
|
|
1654
|
+
},
|
|
1655
|
+
{
|
|
1656
|
+
name: SPLIT_ROUTE_LABEL,
|
|
1657
|
+
color: "0e8a16",
|
|
1658
|
+
description: "A child issue created by a split (split-assessment.md)"
|
|
1659
|
+
},
|
|
1660
|
+
{
|
|
1661
|
+
name: TIER_A_ROUTE_LABEL,
|
|
1662
|
+
color: "d93f0b",
|
|
1663
|
+
description: "Filed Tier A during implementation \u2014 an upstream defect or a prerequisite"
|
|
1664
|
+
}
|
|
1665
|
+
];
|
|
1666
|
+
var DEPTH_0_LABEL = "depth:0";
|
|
1667
|
+
var DEPTH_1_LABEL = "depth:1";
|
|
1668
|
+
var DEPTH_2_LABEL = "depth:2";
|
|
1669
|
+
var DEPTH_LABELS = [
|
|
1670
|
+
{
|
|
1671
|
+
name: DEPTH_0_LABEL,
|
|
1672
|
+
color: "0e8a16",
|
|
1673
|
+
description: "Depth 0 \u2014 what a consumer of this package touches (SKILL.md \xA72i)"
|
|
1674
|
+
},
|
|
1675
|
+
{
|
|
1676
|
+
name: DEPTH_1_LABEL,
|
|
1677
|
+
color: "fbc02d",
|
|
1678
|
+
description: "Depth 1 \u2014 the run orchestration that executes an Issue (SKILL.md \xA72i)"
|
|
1679
|
+
},
|
|
1680
|
+
{
|
|
1681
|
+
name: DEPTH_2_LABEL,
|
|
1682
|
+
color: "c5def5",
|
|
1683
|
+
description: "Depth 2 \u2014 what measures a run (SKILL.md \xA72i)"
|
|
1684
|
+
}
|
|
1685
|
+
];
|
|
1686
|
+
var WORKFLOW_LABELS = [
|
|
1687
|
+
{
|
|
1688
|
+
name: EPIC_LABEL,
|
|
1689
|
+
color: "5319e7",
|
|
1690
|
+
description: "Tracks a batch of child issues from one split"
|
|
1691
|
+
},
|
|
1692
|
+
{ name: IN_PROGRESS_LABEL, color: "0075ca", description: "Work is actively in progress" },
|
|
1693
|
+
...FILING_ROUTE_LABELS,
|
|
1694
|
+
...DEPTH_LABELS
|
|
1695
|
+
];
|
|
1635
1696
|
function has_any_label(labels, wanted) {
|
|
1636
1697
|
return (labels ?? []).some((label) => wanted.has(label.name.toLowerCase()));
|
|
1637
1698
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
investigation_reads
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-HJXQZ7V4.js";
|
|
4
4
|
import {
|
|
5
5
|
lane_guard_policy
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-XSL4SRMA.js";
|
|
7
7
|
import {
|
|
8
8
|
hook_decision
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-LDQDZYHX.js";
|
|
10
10
|
|
|
11
11
|
// scripts/delegation/investigation-guard.ts
|
|
12
12
|
import { text } from "node:stream/consumers";
|
|
@@ -35,31 +35,31 @@ import {
|
|
|
35
35
|
run_watcher_guard,
|
|
36
36
|
shell_segments,
|
|
37
37
|
tail_commands
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-IJPBTDTB.js";
|
|
39
39
|
import {
|
|
40
40
|
find_local_bin_upwards
|
|
41
41
|
} from "./chunk-3V37IID6.js";
|
|
42
42
|
import {
|
|
43
43
|
batch_outcome
|
|
44
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-244WYOEO.js";
|
|
45
45
|
import {
|
|
46
46
|
investigation_refusal
|
|
47
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-KOCK5FXQ.js";
|
|
48
48
|
import {
|
|
49
49
|
duplicate_read_outcome
|
|
50
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-GNOITLRR.js";
|
|
51
51
|
import {
|
|
52
52
|
investigation_reads
|
|
53
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-HJXQZ7V4.js";
|
|
54
54
|
import {
|
|
55
55
|
lane_guard_policy,
|
|
56
56
|
time_batch_guard
|
|
57
|
-
} from "./chunk-
|
|
57
|
+
} from "./chunk-XSL4SRMA.js";
|
|
58
58
|
import {
|
|
59
59
|
hook_decision,
|
|
60
60
|
lane_child_marker,
|
|
61
61
|
run_issue_number
|
|
62
|
-
} from "./chunk-
|
|
62
|
+
} from "./chunk-LDQDZYHX.js";
|
|
63
63
|
import {
|
|
64
64
|
PLATFORM_TEMP_ROOT,
|
|
65
65
|
cost_attribute,
|
|
@@ -71,7 +71,7 @@ import {
|
|
|
71
71
|
stamp_file,
|
|
72
72
|
time_density_hook,
|
|
73
73
|
time_shell
|
|
74
|
-
} from "./chunk-
|
|
74
|
+
} from "./chunk-BELI5XSS.js";
|
|
75
75
|
|
|
76
76
|
// scripts/hooks/pretool-guard.ts
|
|
77
77
|
import { text } from "node:stream/consumers";
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
lane_child_marker
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-LDQDZYHX.js";
|
|
4
4
|
import {
|
|
5
5
|
time_bundle_call,
|
|
6
6
|
time_format,
|
|
7
7
|
time_round_trips,
|
|
8
8
|
time_spans,
|
|
9
9
|
time_transcript_line
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-BELI5XSS.js";
|
|
11
11
|
|
|
12
12
|
// scripts/lane/lane-guard-policy.ts
|
|
13
13
|
var DEFAULT_MODE = "refuse";
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import {
|
|
3
3
|
pretool_guard
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-M6K7EQDO.js";
|
|
5
|
+
import "./chunk-IJPBTDTB.js";
|
|
6
6
|
import {
|
|
7
7
|
build_envelope,
|
|
8
8
|
compose_context,
|
|
9
9
|
format_edited_payload
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-3ON7HOYB.js";
|
|
11
11
|
import "./chunk-3V37IID6.js";
|
|
12
|
-
import "./chunk-
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
16
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-244WYOEO.js";
|
|
13
|
+
import "./chunk-KOCK5FXQ.js";
|
|
14
|
+
import "./chunk-GNOITLRR.js";
|
|
15
|
+
import "./chunk-HJXQZ7V4.js";
|
|
16
|
+
import "./chunk-XSL4SRMA.js";
|
|
17
17
|
import {
|
|
18
18
|
hook_decision
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-LDQDZYHX.js";
|
|
20
20
|
import {
|
|
21
21
|
time_density_hook
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-BELI5XSS.js";
|
|
23
23
|
import "./chunk-WPALEIRX.js";
|
|
24
24
|
import "./chunk-RKUAD3I6.js";
|
|
25
25
|
|
|
@@ -9,11 +9,11 @@ import {
|
|
|
9
9
|
is_enabled,
|
|
10
10
|
notice_refusal_path,
|
|
11
11
|
refusal_path
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
16
|
-
import "./chunk-
|
|
12
|
+
} from "./chunk-GNOITLRR.js";
|
|
13
|
+
import "./chunk-HJXQZ7V4.js";
|
|
14
|
+
import "./chunk-XSL4SRMA.js";
|
|
15
|
+
import "./chunk-LDQDZYHX.js";
|
|
16
|
+
import "./chunk-BELI5XSS.js";
|
|
17
17
|
import "./chunk-WPALEIRX.js";
|
|
18
18
|
import "./chunk-RKUAD3I6.js";
|
|
19
19
|
export {
|
|
@@ -15,9 +15,9 @@ import {
|
|
|
15
15
|
relative_to_root,
|
|
16
16
|
resolve_invocations,
|
|
17
17
|
select_invocation
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-3ON7HOYB.js";
|
|
19
19
|
import "./chunk-3V37IID6.js";
|
|
20
|
-
import "./chunk-
|
|
20
|
+
import "./chunk-BELI5XSS.js";
|
|
21
21
|
import "./chunk-RKUAD3I6.js";
|
|
22
22
|
export {
|
|
23
23
|
ESLINT_DAEMON,
|
|
@@ -10,11 +10,11 @@ import {
|
|
|
10
10
|
is_enabled,
|
|
11
11
|
load_environment_file,
|
|
12
12
|
refusal_path
|
|
13
|
-
} from "./chunk-
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
16
|
-
import "./chunk-
|
|
17
|
-
import "./chunk-
|
|
13
|
+
} from "./chunk-KOCK5FXQ.js";
|
|
14
|
+
import "./chunk-HJXQZ7V4.js";
|
|
15
|
+
import "./chunk-XSL4SRMA.js";
|
|
16
|
+
import "./chunk-LDQDZYHX.js";
|
|
17
|
+
import "./chunk-BELI5XSS.js";
|
|
18
18
|
import "./chunk-WPALEIRX.js";
|
|
19
19
|
import "./chunk-RKUAD3I6.js";
|
|
20
20
|
export {
|
|
@@ -3,16 +3,16 @@ import {
|
|
|
3
3
|
pretool_guard,
|
|
4
4
|
pretool_outcome,
|
|
5
5
|
pretool_outcome_async
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-M6K7EQDO.js";
|
|
7
|
+
import "./chunk-IJPBTDTB.js";
|
|
8
8
|
import "./chunk-3V37IID6.js";
|
|
9
|
-
import "./chunk-
|
|
10
|
-
import "./chunk-
|
|
11
|
-
import "./chunk-
|
|
12
|
-
import "./chunk-
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
9
|
+
import "./chunk-244WYOEO.js";
|
|
10
|
+
import "./chunk-KOCK5FXQ.js";
|
|
11
|
+
import "./chunk-GNOITLRR.js";
|
|
12
|
+
import "./chunk-HJXQZ7V4.js";
|
|
13
|
+
import "./chunk-XSL4SRMA.js";
|
|
14
|
+
import "./chunk-LDQDZYHX.js";
|
|
15
|
+
import "./chunk-BELI5XSS.js";
|
|
16
16
|
import "./chunk-WPALEIRX.js";
|
|
17
17
|
import "./chunk-RKUAD3I6.js";
|
|
18
18
|
export {
|
package/dist/hooks/stop-guard.js
CHANGED
|
@@ -16,20 +16,20 @@ import {
|
|
|
16
16
|
run_hold,
|
|
17
17
|
run_invocation,
|
|
18
18
|
run_watcher_guard
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-IJPBTDTB.js";
|
|
20
20
|
import {
|
|
21
21
|
session_language
|
|
22
22
|
} from "./chunk-6V5OPIUU.js";
|
|
23
23
|
import {
|
|
24
24
|
hook_decision,
|
|
25
25
|
lane_child_marker
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-LDQDZYHX.js";
|
|
27
27
|
import {
|
|
28
28
|
PROJECT_ROOT,
|
|
29
29
|
stamp_file,
|
|
30
30
|
time_density_hook,
|
|
31
31
|
time_hook_transcript
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-BELI5XSS.js";
|
|
33
33
|
import "./chunk-WPALEIRX.js";
|
|
34
34
|
import "./chunk-RKUAD3I6.js";
|
|
35
35
|
|
package/dist/josh.js
CHANGED
|
@@ -1173,9 +1173,13 @@ var PROJECT_COMMANDS = {
|
|
|
1173
1173
|
},
|
|
1174
1174
|
start: {
|
|
1175
1175
|
script: "scripts/init/start.ts",
|
|
1176
|
-
description: "
|
|
1176
|
+
description: "Create a new project for the GitHub Issue workflow, from git init to labels",
|
|
1177
1177
|
category: "Project",
|
|
1178
|
-
reference: [
|
|
1178
|
+
reference: [
|
|
1179
|
+
`${PROFILE_ARGUMENTS} [--yes] [--github] [--public]`,
|
|
1180
|
+
"developer",
|
|
1181
|
+
["files", "git", "network", "processes"]
|
|
1182
|
+
]
|
|
1179
1183
|
},
|
|
1180
1184
|
init: {
|
|
1181
1185
|
script: "scripts/init/init.ts",
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
2
|
+
"version": "1.957.0",
|
|
3
3
|
"name": "@joshuafolkken/kit",
|
|
4
4
|
"description": "Development toolkit for AI-assisted projects: formatting, AI assistant rules and a Git/GitHub Issue workflow for any repository, plus lint, type check, tests and hooks for Node projects",
|
|
5
5
|
"keywords": [
|
|
@@ -3,6 +3,7 @@ import { setTimeout as sleep } from 'node:timers/promises'
|
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
import { parseArgs } from 'node:util'
|
|
5
5
|
import { CONTEXT_CUT_THRESHOLD } from '#scripts/cost-runtime/context-cut-threshold'
|
|
6
|
+
import { hook_decision } from '#scripts/josh/hook-decision'
|
|
6
7
|
import { josh_command } from '#scripts/josh/josh-run'
|
|
7
8
|
import { lane_await, type AwaitState } from '#scripts/lane/lane-await'
|
|
8
9
|
import { lane_launch_cli } from '#scripts/lane/lane-launch-cli'
|
|
@@ -24,6 +25,7 @@ import { backlog_drive_named_offer } from './backlog-drive-named-offer'
|
|
|
24
25
|
import { backlog_drive_offer_argv } from './backlog-drive-offer-argv'
|
|
25
26
|
import { backlog_drive_owner } from './backlog-drive-owner'
|
|
26
27
|
import { backlog_drive_restore } from './backlog-drive-restore'
|
|
28
|
+
import { backlog_drive_retrospective } from './backlog-drive-retrospective'
|
|
27
29
|
import { backlog_offer_cli } from './backlog-offer-cli'
|
|
28
30
|
|
|
29
31
|
// `josh backlog:drive` — the backlogrun parent's loop as one resident wait (joshuafolkken/kit#2499).
|
|
@@ -193,7 +195,7 @@ async function read_offer(
|
|
|
193
195
|
|
|
194
196
|
return offer === undefined
|
|
195
197
|
? undefined
|
|
196
|
-
: { ...offer,
|
|
198
|
+
: { ...offer, is_retrospective_owed: backlog_drive_retrospective.is_owed(carry) }
|
|
197
199
|
}
|
|
198
200
|
|
|
199
201
|
// A non-zero `run:merge` prints its token first only for a hand-off (`busy`, `stop`, `retry`); any
|
|
@@ -357,6 +359,8 @@ async function run(argv: ReadonlyArray<string>): Promise<number> {
|
|
|
357
359
|
}
|
|
358
360
|
|
|
359
361
|
async function main(argv: ReadonlyArray<string>): Promise<void> {
|
|
362
|
+
// Load `.env` on the real command path only, so `JOSH_RETROSPECTIVE` set there reaches the offer read.
|
|
363
|
+
hook_decision.load_environment_file()
|
|
360
364
|
process.exitCode = await run(argv)
|
|
361
365
|
}
|
|
362
366
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { doctor_consumer } from '#scripts/doctor/doctor-consumer'
|
|
2
|
+
import { find_package_directory } from '#scripts/josh/josh-logic'
|
|
3
|
+
import type { RunCarry } from '#scripts/run/run-carry'
|
|
4
|
+
import { run_retrospective } from '#scripts/run/run-retrospective'
|
|
5
|
+
|
|
6
|
+
// Whether a drain owes the retrospective, read through the owed rule `run:step` reads, so a drain it
|
|
7
|
+
// owes nothing at keeps the idle watch rather than waking a session with nothing to run
|
|
8
|
+
// (joshuafolkken/kit#2750). The drive is the parent's loop, never a lane child's.
|
|
9
|
+
function is_owed(carry: RunCarry): boolean {
|
|
10
|
+
const root = find_package_directory(process.cwd())
|
|
11
|
+
|
|
12
|
+
return run_retrospective.is_owed({
|
|
13
|
+
is_retrospective_enabled: run_retrospective.is_enabled(),
|
|
14
|
+
is_lane_child: false,
|
|
15
|
+
is_retrospective_done: carry.retrospective === true,
|
|
16
|
+
is_consumer: doctor_consumer.is_kit_consumer(root),
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const backlog_drive_retrospective = { is_owed }
|
|
@@ -45,7 +45,9 @@ interface OfferRead {
|
|
|
45
45
|
retries: number
|
|
46
46
|
answer?: string | undefined
|
|
47
47
|
reason?: string | undefined
|
|
48
|
-
|
|
48
|
+
// Whether a drain here owes the retrospective: the opt-in switch is on and it has not run yet. Absent
|
|
49
|
+
// reads as not owed, so a drain with the switch off keeps the idle watch (joshuafolkken/kit#2750).
|
|
50
|
+
is_retrospective_owed?: boolean
|
|
49
51
|
is_finish?: boolean | undefined
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -106,7 +108,9 @@ function ended(reason: string, state: DriveState, token?: string, issue?: string
|
|
|
106
108
|
|
|
107
109
|
function is_drain(read: OfferRead, state: DriveState): boolean {
|
|
108
110
|
return (
|
|
109
|
-
read.verdict === WATCH_VERDICT &&
|
|
111
|
+
read.verdict === WATCH_VERDICT &&
|
|
112
|
+
state.in_flight.length === 0 &&
|
|
113
|
+
read.is_retrospective_owed === true
|
|
110
114
|
)
|
|
111
115
|
}
|
|
112
116
|
|
|
@@ -115,7 +119,7 @@ function needs_retrospective(read: OfferRead, state: DriveState): boolean {
|
|
|
115
119
|
read.verdict === STOP_VERDICT &&
|
|
116
120
|
read.answer === 'exhausted' &&
|
|
117
121
|
state.in_flight.length === 0 &&
|
|
118
|
-
|
|
122
|
+
read.is_retrospective_owed === true
|
|
119
123
|
)
|
|
120
124
|
}
|
|
121
125
|
|
|
@@ -177,6 +177,24 @@ const DEPTH_LABELS: ReadonlyArray<{
|
|
|
177
177
|
},
|
|
178
178
|
]
|
|
179
179
|
|
|
180
|
+
// What `josh start` provisions on a new repository so the first `kickoff` finds every label a run
|
|
181
|
+
// applies with its intended color (joshuafolkken/kit#2197). The `epic` and `in-progress` metadata is
|
|
182
|
+
// the one the workflow documents create them with; the labels only a person applies are left out.
|
|
183
|
+
const WORKFLOW_LABELS: ReadonlyArray<{
|
|
184
|
+
name: string
|
|
185
|
+
color: string
|
|
186
|
+
description: string
|
|
187
|
+
}> = [
|
|
188
|
+
{
|
|
189
|
+
name: EPIC_LABEL,
|
|
190
|
+
color: '5319e7',
|
|
191
|
+
description: 'Tracks a batch of child issues from one split',
|
|
192
|
+
},
|
|
193
|
+
{ name: IN_PROGRESS_LABEL, color: '0075ca', description: 'Work is actively in progress' },
|
|
194
|
+
...FILING_ROUTE_LABELS,
|
|
195
|
+
...DEPTH_LABELS,
|
|
196
|
+
]
|
|
197
|
+
|
|
180
198
|
// The shape `gh issue list --json labels` returns; narrowed here so the predicate below takes any
|
|
181
199
|
// listing row without importing a schema.
|
|
182
200
|
interface LabelReference {
|
|
@@ -285,4 +303,5 @@ export {
|
|
|
285
303
|
REVIEW_ROUND2_SKIPPED_LABEL,
|
|
286
304
|
SPLIT_ROUTE_LABEL,
|
|
287
305
|
TIER_A_ROUTE_LABEL,
|
|
306
|
+
WORKFLOW_LABELS,
|
|
288
307
|
}
|
|
@@ -29,12 +29,16 @@ function parse_profile(value: unknown): ProjectProfile | undefined {
|
|
|
29
29
|
return undefined
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function
|
|
33
|
-
|
|
32
|
+
function is_profile_pair(args: ReadonlyArray<string>): boolean {
|
|
33
|
+
return args.length === PROFILE_ARG_LENGTH && args[0] === '--profile'
|
|
34
|
+
}
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
function requested_profile(
|
|
37
|
+
args: ReadonlyArray<string>,
|
|
38
|
+
usage = 'josh init [--profile static|node]',
|
|
39
|
+
): ProjectProfile | undefined {
|
|
40
|
+
if (args.length === 0) return undefined
|
|
41
|
+
if (!is_profile_pair(args)) throw new Error(`Usage: ${usage}`)
|
|
38
42
|
|
|
39
43
|
const profile = parse_profile(args[1])
|
|
40
44
|
if (profile !== undefined) return profile
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { ProjectProfile } from './project-profile'
|
|
2
|
+
|
|
3
|
+
type Visibility = 'private' | 'public'
|
|
4
|
+
|
|
5
|
+
interface StartOptions {
|
|
6
|
+
profile: ProjectProfile | undefined
|
|
7
|
+
is_yes: boolean
|
|
8
|
+
is_github: boolean
|
|
9
|
+
visibility: Visibility
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type StartStep = 'git_init' | 'initialize' | 'commit' | 'repository' | 'labels'
|
|
13
|
+
|
|
14
|
+
interface GitState {
|
|
15
|
+
has_git: boolean
|
|
16
|
+
has_github: boolean
|
|
17
|
+
has_origin: boolean
|
|
18
|
+
branch: string | undefined
|
|
19
|
+
has_commits: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface StepPlan {
|
|
23
|
+
steps: ReadonlyArray<StartStep>
|
|
24
|
+
refusal: string | undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type Interaction = 'interactive' | 'unattended'
|
|
28
|
+
type Consent = 'granted' | 'ask'
|
|
29
|
+
|
|
30
|
+
interface Verdict<T> {
|
|
31
|
+
value: T | undefined
|
|
32
|
+
refusal: string | undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DEFAULT_BRANCH = 'main'
|
|
36
|
+
const FULL_STEPS: ReadonlyArray<StartStep> = [
|
|
37
|
+
'git_init',
|
|
38
|
+
'initialize',
|
|
39
|
+
'commit',
|
|
40
|
+
'repository',
|
|
41
|
+
'labels',
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
const STEP_LABELS: Readonly<Record<StartStep, string>> = {
|
|
45
|
+
git_init: 'Create a Git repository on main',
|
|
46
|
+
initialize: 'Initialize kit (the same setup as josh init)',
|
|
47
|
+
commit: 'Commit every file as the initial commit',
|
|
48
|
+
repository: 'Create the GitHub repository and push main',
|
|
49
|
+
labels: 'Add the workflow labels the repository is missing',
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function refused(refusal: string): StepPlan {
|
|
53
|
+
return { steps: [], refusal }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// A repository that already has a history keeps it: nothing is committed on the user's behalf, and
|
|
57
|
+
// only `main` is pushed, because the Issue workflow branches from and merges into it.
|
|
58
|
+
function existing_git_steps(state: GitState): StepPlan {
|
|
59
|
+
if (!state.has_commits) return { steps: FULL_STEPS.slice(1), refusal: undefined }
|
|
60
|
+
|
|
61
|
+
if (state.branch !== DEFAULT_BRANCH) {
|
|
62
|
+
return refused(
|
|
63
|
+
`josh start pushes ${DEFAULT_BRANCH}, but this repository is on ${state.branch ?? 'a detached HEAD'}. Switch to ${DEFAULT_BRANCH}, or run josh init to set up without GitHub.`,
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { steps: ['initialize', 'repository', 'labels'], refusal: undefined }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// An existing GitHub origin is never replaced or pushed to: only the additive steps run. Any other
|
|
71
|
+
// origin is refused here, before the first write — `gh repo create --remote origin` cannot add a
|
|
72
|
+
// remote that already exists, so the run would otherwise fail after initializing the directory.
|
|
73
|
+
function plan_steps(state: GitState): StepPlan {
|
|
74
|
+
if (state.has_github) return { steps: ['initialize', 'labels'], refusal: undefined }
|
|
75
|
+
|
|
76
|
+
if (state.has_origin) {
|
|
77
|
+
return refused(
|
|
78
|
+
'josh start creates the GitHub repository as origin, but this repository already has an origin that is not on GitHub. Remove or rename it, or run josh init to set up without GitHub.',
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (state.has_git) return existing_git_steps(state)
|
|
83
|
+
|
|
84
|
+
return { steps: FULL_STEPS, refusal: undefined }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function interaction_of(options: StartOptions, is_tty: boolean): Verdict<Interaction> {
|
|
88
|
+
if (options.is_yes) return { value: 'unattended', refusal: undefined }
|
|
89
|
+
if (is_tty) return { value: 'interactive', refusal: undefined }
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
value: undefined,
|
|
93
|
+
refusal:
|
|
94
|
+
'josh start needs a terminal to confirm its choices. Pass --yes to accept the detected profile, and --github to create the GitHub repository.',
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// `--yes` accepts the detected defaults, never the creation of a public-facing resource: an
|
|
99
|
+
// unattended run creates and pushes a repository only when `--github` asks for it by name.
|
|
100
|
+
function github_consent(
|
|
101
|
+
steps: ReadonlyArray<StartStep>,
|
|
102
|
+
options: StartOptions,
|
|
103
|
+
interaction: Interaction,
|
|
104
|
+
): Verdict<Consent> {
|
|
105
|
+
if (!steps.includes('repository') || options.is_github) {
|
|
106
|
+
return { value: 'granted', refusal: undefined }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (interaction === 'interactive') return { value: 'ask', refusal: undefined }
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
value: undefined,
|
|
113
|
+
refusal:
|
|
114
|
+
'josh start --yes does not create a GitHub repository or push. Add --github to create one, or run josh init to set up without GitHub.',
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// What a failed run prints, so the user can tell how far it got before deciding how to resume.
|
|
119
|
+
function progress_report(
|
|
120
|
+
failed: StartStep,
|
|
121
|
+
completed: ReadonlyArray<StartStep>,
|
|
122
|
+
cause: string,
|
|
123
|
+
): string {
|
|
124
|
+
const done =
|
|
125
|
+
completed.length === 0 ? 'nothing' : completed.map((step) => STEP_LABELS[step]).join('; ')
|
|
126
|
+
|
|
127
|
+
return `josh start stopped at: ${STEP_LABELS[failed]}\nCompleted: ${done}\nCause: ${cause}`
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function plan_summary(steps: ReadonlyArray<StartStep>): string {
|
|
131
|
+
const lines = steps.map((step, index) => ` ${String(index + 1)}. ${STEP_LABELS[step]}`)
|
|
132
|
+
|
|
133
|
+
return ['josh start will:', ...lines].join('\n')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const start_plan = {
|
|
137
|
+
plan_steps,
|
|
138
|
+
interaction_of,
|
|
139
|
+
github_consent,
|
|
140
|
+
progress_report,
|
|
141
|
+
plan_summary,
|
|
142
|
+
STEP_LABELS,
|
|
143
|
+
}
|
|
144
|
+
export { start_plan }
|
|
145
|
+
export type { Consent, GitState, Interaction, StartOptions, StartStep, StepPlan, Visibility }
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { stdin as input, stdout as output } from 'node:process'
|
|
3
|
+
import { createInterface, type Interface } from 'node:readline/promises'
|
|
4
|
+
import { ask_yes_no_simple } from '#scripts/git/git-prompt'
|
|
5
|
+
import { project_profile, type ProjectProfile, type ProjectShape } from './project-profile'
|
|
6
|
+
import type { Consent, StartOptions } from './start-plan'
|
|
7
|
+
|
|
8
|
+
interface Choices {
|
|
9
|
+
options: StartOptions
|
|
10
|
+
shape: ProjectShape
|
|
11
|
+
consent: Consent
|
|
12
|
+
root: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DECLINED =
|
|
16
|
+
'Stopped before creating the GitHub repository. Nothing was changed. Run josh init to set up without GitHub.'
|
|
17
|
+
|
|
18
|
+
// The detected profile is the default, so pressing Enter accepts it; the reason is shown because an
|
|
19
|
+
// ambiguous project is exactly the case this question exists for.
|
|
20
|
+
async function ask_profile(prompt: Interface, shape: ProjectShape): Promise<ProjectProfile> {
|
|
21
|
+
const question = `Profile — static or node [${shape.profile}, ${shape.reason}]: `
|
|
22
|
+
const raw_answer = await prompt.question(question)
|
|
23
|
+
const answer = raw_answer.trim()
|
|
24
|
+
if (answer.length === 0) return shape.profile
|
|
25
|
+
|
|
26
|
+
return project_profile.requested_profile(['--profile', answer]) ?? shape.profile
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function ask_repository(prompt: Interface, choices: Choices): Promise<void> {
|
|
30
|
+
const name = path.basename(choices.root)
|
|
31
|
+
const question = `Create a ${choices.options.visibility} GitHub repository "${name}" and push main? (y/n): `
|
|
32
|
+
|
|
33
|
+
if (!(await ask_yes_no_simple(prompt, question))) throw new Error(DECLINED)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function ask_choices(prompt: Interface, choices: Choices): Promise<ProjectProfile> {
|
|
37
|
+
const profile = choices.options.profile ?? (await ask_profile(prompt, choices.shape))
|
|
38
|
+
if (choices.consent === 'ask') await ask_repository(prompt, choices)
|
|
39
|
+
|
|
40
|
+
return profile
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function confirm_choices(choices: Choices): Promise<ProjectProfile> {
|
|
44
|
+
const prompt = createInterface({ input, output })
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
return await ask_choices(prompt, choices)
|
|
48
|
+
} finally {
|
|
49
|
+
prompt.close()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const start_prompt = { confirm_choices }
|
|
54
|
+
export { start_prompt }
|
|
55
|
+
export type { Choices }
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { git_gh_exec } from '#scripts/git/git-gh-exec'
|
|
3
|
+
import { WORKFLOW_LABELS } from '#scripts/git/issue-labels'
|
|
4
|
+
import { self_sync_guard } from '#scripts/self-sync-guard/self-sync-guard-logic'
|
|
5
|
+
import { execaSync } from 'execa'
|
|
6
|
+
import { main as init_main } from './init'
|
|
7
|
+
import { PACKAGE_DIR } from './init-paths'
|
|
8
|
+
import type { ProjectProfile, ProjectShape } from './project-profile'
|
|
9
|
+
import { start_plan, type GitState, type StartStep, type Visibility } from './start-plan'
|
|
10
|
+
|
|
11
|
+
interface StepContext {
|
|
12
|
+
root: string
|
|
13
|
+
profile: ProjectProfile
|
|
14
|
+
visibility: Visibility
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type WorkflowLabel = (typeof WORKFLOW_LABELS)[number]
|
|
18
|
+
|
|
19
|
+
const DEFAULT_BRANCH = 'main'
|
|
20
|
+
const LABELS_ENDPOINT = 'repos/{owner}/{repo}/labels'
|
|
21
|
+
const GH_INSTALL_HINT =
|
|
22
|
+
'GitHub CLI (gh) is not installed. Install it from https://cli.github.com/, run gh auth login, then run josh start again. Nothing was changed.'
|
|
23
|
+
const GH_AUTH_HINT =
|
|
24
|
+
'GitHub CLI is not signed in. Run gh auth login, then run josh start again. Nothing was changed.'
|
|
25
|
+
|
|
26
|
+
function succeeds(command: string, args: ReadonlyArray<string>, root: string): boolean {
|
|
27
|
+
return execaSync(command, args, { cwd: root, reject: false }).exitCode === 0
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function read_output(
|
|
31
|
+
command: string,
|
|
32
|
+
args: ReadonlyArray<string>,
|
|
33
|
+
root: string,
|
|
34
|
+
): string | undefined {
|
|
35
|
+
const result = execaSync(command, args, { cwd: root, reject: false })
|
|
36
|
+
|
|
37
|
+
return result.exitCode === 0 ? result.stdout.trim() : undefined
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Every step spawned here dials GitHub directly, past a scanner's loopback proxy, the same as every
|
|
41
|
+
// other `gh` spawn under scripts/ (joshuafolkken/kit#2436); a local `git` step is unaffected by it.
|
|
42
|
+
function run(command: string, args: ReadonlyArray<string>, root: string): void {
|
|
43
|
+
execaSync(command, args, { ...git_gh_exec.direct_environment(), cwd: root, stdio: 'inherit' })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function read_git_state(root: string, shape: ProjectShape): GitState {
|
|
47
|
+
const { has_git, has_github } = shape
|
|
48
|
+
|
|
49
|
+
if (!has_git) {
|
|
50
|
+
return { has_git, has_github, has_origin: false, branch: undefined, has_commits: false }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const has_origin = succeeds('git', ['remote', 'get-url', 'origin'], root)
|
|
54
|
+
const branch = read_output('git', ['symbolic-ref', '--short', 'HEAD'], root)
|
|
55
|
+
const has_commits = succeeds('git', ['rev-parse', '--verify', '--quiet', 'HEAD'], root)
|
|
56
|
+
|
|
57
|
+
return { has_git, has_github, has_origin, branch, has_commits }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Every step talks to GitHub, so the CLI is checked before the first write rather than at the step
|
|
61
|
+
// that needs it — a missing `gh` then leaves the directory exactly as it was.
|
|
62
|
+
function github_cli_refusal(root: string): string | undefined {
|
|
63
|
+
if (!succeeds('gh', ['--version'], root)) return GH_INSTALL_HINT
|
|
64
|
+
if (!succeeds('gh', ['auth', 'status'], root)) return GH_AUTH_HINT
|
|
65
|
+
|
|
66
|
+
return undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// `josh init` refuses to run inside the kit source repository by printing and setting the exit code
|
|
70
|
+
// rather than throwing, so its step would pass and the later steps would commit and publish the
|
|
71
|
+
// source repository. The same check is raised before the first write instead.
|
|
72
|
+
function self_run_refusal(root: string): string | undefined {
|
|
73
|
+
return self_sync_guard.self_sync_refusal(PACKAGE_DIR, root)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function create_git_repository(context: StepContext): void {
|
|
77
|
+
run('git', ['init', `--initial-branch=${DEFAULT_BRANCH}`], context.root)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// The same setup `josh init` runs, called rather than copied, with the profile the user confirmed.
|
|
81
|
+
function initialize(context: StepContext): void {
|
|
82
|
+
init_main(['--profile', context.profile])
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function commit_all(context: StepContext): void {
|
|
86
|
+
run('git', ['add', '--all'], context.root)
|
|
87
|
+
// The first commit of a new repository has no branch to come from, so the hook that keeps commits
|
|
88
|
+
// off main — installed by the initialize step in a node project — would refuse the only commit
|
|
89
|
+
// that has to land there.
|
|
90
|
+
run('git', ['commit', '--no-verify', '--message', 'Initial commit'], context.root)
|
|
91
|
+
// A repository created by an older `git init` may name its unborn branch `master`.
|
|
92
|
+
run('git', ['branch', '--move', '--force', DEFAULT_BRANCH], context.root)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function create_github_repository(context: StepContext): void {
|
|
96
|
+
const name = path.basename(context.root)
|
|
97
|
+
const visibility = `--${context.visibility}`
|
|
98
|
+
const source = ['--source', '.', '--remote', 'origin', '--push']
|
|
99
|
+
|
|
100
|
+
run('gh', ['repo', 'create', name, visibility, ...source], context.root)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Labels already present are left as they are — only the missing ones are created, so a repository
|
|
104
|
+
// that recolored a label keeps its choice. GitHub compares label names case-insensitively.
|
|
105
|
+
function missing_workflow_labels(existing: ReadonlyArray<string>): ReadonlyArray<WorkflowLabel> {
|
|
106
|
+
const names = new Set(existing.map((name) => name.toLowerCase()))
|
|
107
|
+
|
|
108
|
+
return WORKFLOW_LABELS.filter((label) => !names.has(label.name))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function create_label(label: WorkflowLabel, root: string): void {
|
|
112
|
+
const fields = [`name=${label.name}`, `color=${label.color}`, `description=${label.description}`]
|
|
113
|
+
|
|
114
|
+
run('gh', ['api', LABELS_ENDPOINT, ...fields.flatMap((field) => ['-f', field]), '--silent'], root)
|
|
115
|
+
console.info(` ✔ label ${label.name}`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function add_missing_labels(context: StepContext): void {
|
|
119
|
+
const listing = execaSync('gh', ['api', '--paginate', LABELS_ENDPOINT, '--jq', '.[].name'], {
|
|
120
|
+
...git_gh_exec.direct_environment(),
|
|
121
|
+
cwd: context.root,
|
|
122
|
+
})
|
|
123
|
+
const existing = listing.stdout.split('\n').filter((name) => name.length > 0)
|
|
124
|
+
|
|
125
|
+
for (const label of missing_workflow_labels(existing)) create_label(label, context.root)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const STEP_ACTIONS: Readonly<Record<StartStep, (context: StepContext) => void>> = {
|
|
129
|
+
git_init: create_git_repository,
|
|
130
|
+
initialize,
|
|
131
|
+
commit: commit_all,
|
|
132
|
+
repository: create_github_repository,
|
|
133
|
+
labels: add_missing_labels,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function run_step(
|
|
137
|
+
step: StartStep,
|
|
138
|
+
context: StepContext,
|
|
139
|
+
completed: ReadonlyArray<StartStep>,
|
|
140
|
+
): void {
|
|
141
|
+
try {
|
|
142
|
+
STEP_ACTIONS[step](context)
|
|
143
|
+
} catch (error) {
|
|
144
|
+
const cause = error instanceof Error ? error.message : String(error)
|
|
145
|
+
|
|
146
|
+
throw new Error(start_plan.progress_report(step, completed, cause), { cause: error })
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function run_steps(steps: ReadonlyArray<StartStep>, context: StepContext): void {
|
|
151
|
+
for (const [index, step] of steps.entries()) {
|
|
152
|
+
const position = `${String(index + 1)}/${String(steps.length)}`
|
|
153
|
+
|
|
154
|
+
console.info(`\n[${position}] ${start_plan.STEP_LABELS[step]}`)
|
|
155
|
+
run_step(step, context, steps.slice(0, index))
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const start_steps = {
|
|
160
|
+
read_git_state,
|
|
161
|
+
github_cli_refusal,
|
|
162
|
+
self_run_refusal,
|
|
163
|
+
missing_workflow_labels,
|
|
164
|
+
run_steps,
|
|
165
|
+
}
|
|
166
|
+
export { start_steps }
|
|
167
|
+
export type { StepContext }
|
package/scripts/init/start.ts
CHANGED
|
@@ -1,30 +1,106 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { project_profile, type ProjectProfile } from './project-profile'
|
|
4
|
+
import {
|
|
5
|
+
start_plan,
|
|
6
|
+
type Consent,
|
|
7
|
+
type Interaction,
|
|
8
|
+
type StartOptions,
|
|
9
|
+
type StartStep,
|
|
10
|
+
} from './start-plan'
|
|
11
|
+
import { start_prompt, type Choices } from './start-prompt'
|
|
12
|
+
import { start_steps } from './start-steps'
|
|
5
13
|
|
|
6
14
|
const ARGUMENT_START_INDEX = 2
|
|
15
|
+
const READY_MESSAGE = 'GitHub workflow ready. Use kickoff new in your assistant to plan an Issue.'
|
|
16
|
+
const START_USAGE = 'josh start [--profile static|node] [--yes] [--github] [--public]'
|
|
17
|
+
const YES_FLAG = '--yes'
|
|
18
|
+
const GITHUB_FLAG = '--github'
|
|
19
|
+
const PUBLIC_FLAG = '--public'
|
|
20
|
+
const SWITCHES: ReadonlySet<string> = new Set([YES_FLAG, GITHUB_FLAG, PUBLIC_FLAG])
|
|
7
21
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
22
|
+
interface Prepared {
|
|
23
|
+
steps: ReadonlyArray<StartStep>
|
|
24
|
+
interaction: Interaction
|
|
25
|
+
choices: Choices
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The switches are peeled off first, so what remains is exactly the `--profile` pair `josh init`
|
|
29
|
+
// accepts — one parser for the profile in both commands.
|
|
30
|
+
function parse_start_options(args: ReadonlyArray<string>): StartOptions {
|
|
31
|
+
const profile_args = args.filter((argument) => !SWITCHES.has(argument))
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
profile: project_profile.requested_profile(profile_args, START_USAGE),
|
|
35
|
+
is_yes: args.includes(YES_FLAG),
|
|
36
|
+
is_github: args.includes(GITHUB_FLAG),
|
|
37
|
+
visibility: args.includes(PUBLIC_FLAG) ? 'public' : 'private',
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function settle<T>(verdict: { value: T | undefined; refusal: string | undefined }): T {
|
|
42
|
+
if (verdict.value === undefined) throw new Error(verdict.refusal)
|
|
11
43
|
|
|
12
|
-
return
|
|
44
|
+
return verdict.value
|
|
13
45
|
}
|
|
14
46
|
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
47
|
+
function stop_on(refusal: string | undefined): void {
|
|
48
|
+
if (refusal !== undefined) throw new Error(refusal)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// An unattended run already holds every answer: `--profile` or the detected one, and a consent that
|
|
52
|
+
// `github_consent` only grants when `--github` was given.
|
|
53
|
+
async function resolve_profile(
|
|
54
|
+
choices: Choices,
|
|
55
|
+
interaction: Interaction,
|
|
56
|
+
): Promise<ProjectProfile> {
|
|
57
|
+
if (interaction === 'unattended') return choices.options.profile ?? choices.shape.profile
|
|
58
|
+
|
|
59
|
+
return await start_prompt.confirm_choices(choices)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Every refusal is raised here, before the first write, so a run that stops here changed nothing.
|
|
63
|
+
function prepare(args: ReadonlyArray<string>, is_tty: boolean, root: string): Prepared {
|
|
64
|
+
stop_on(start_steps.self_run_refusal(root))
|
|
65
|
+
const options = parse_start_options(args)
|
|
66
|
+
const interaction = settle(start_plan.interaction_of(options, is_tty))
|
|
67
|
+
const shape = project_profile.inspect_project(root, options.profile)
|
|
68
|
+
const plan = start_plan.plan_steps(start_steps.read_git_state(root, shape))
|
|
69
|
+
|
|
70
|
+
stop_on(plan.refusal)
|
|
71
|
+
const consent: Consent = settle(start_plan.github_consent(plan.steps, options, interaction))
|
|
72
|
+
|
|
73
|
+
stop_on(start_steps.github_cli_refusal(root))
|
|
74
|
+
|
|
75
|
+
return { steps: plan.steps, interaction, choices: { options, shape, consent, root } }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function main(args: ReadonlyArray<string>, is_tty: boolean): Promise<void> {
|
|
79
|
+
const { steps, interaction, choices } = prepare(args, is_tty, process.cwd())
|
|
80
|
+
|
|
81
|
+
console.info(start_plan.plan_summary(steps))
|
|
82
|
+
const profile = await resolve_profile(choices, interaction)
|
|
83
|
+
const { root, options } = choices
|
|
84
|
+
|
|
85
|
+
start_steps.run_steps(steps, { root, profile, visibility: options.visibility })
|
|
86
|
+
console.info(`\n${READY_MESSAGE}`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function run(args: ReadonlyArray<string>, is_tty: boolean): Promise<number> {
|
|
90
|
+
try {
|
|
91
|
+
await main(args, is_tty)
|
|
92
|
+
|
|
93
|
+
return 0
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(error instanceof Error ? error.message : String(error))
|
|
96
|
+
|
|
97
|
+
return 1
|
|
98
|
+
}
|
|
23
99
|
}
|
|
24
100
|
|
|
25
101
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
26
|
-
|
|
102
|
+
process.exitCode = await run(process.argv.slice(ARGUMENT_START_INDEX), process.stdin.isTTY)
|
|
27
103
|
}
|
|
28
104
|
|
|
29
|
-
const start = {
|
|
105
|
+
const start = { run, parse_start_options }
|
|
30
106
|
export { start }
|
|
@@ -1103,12 +1103,13 @@ exports[`COMMAND_MAP — reference metadata > keeps every canonical command and
|
|
|
1103
1103
|
],
|
|
1104
1104
|
],
|
|
1105
1105
|
"start": [
|
|
1106
|
-
"[--profile static|node]",
|
|
1106
|
+
"[--profile static|node] [--yes] [--github] [--public]",
|
|
1107
1107
|
"developer",
|
|
1108
1108
|
[
|
|
1109
1109
|
"files",
|
|
1110
1110
|
"git",
|
|
1111
1111
|
"network",
|
|
1112
|
+
"processes",
|
|
1112
1113
|
],
|
|
1113
1114
|
],
|
|
1114
1115
|
"stash:pop": [
|
|
@@ -14,9 +14,13 @@ const PROJECT_COMMANDS: Record<string, CommandEntry> = {
|
|
|
14
14
|
},
|
|
15
15
|
start: {
|
|
16
16
|
script: 'scripts/init/start.ts',
|
|
17
|
-
description: '
|
|
17
|
+
description: 'Create a new project for the GitHub Issue workflow, from git init to labels',
|
|
18
18
|
category: 'Project',
|
|
19
|
-
reference: [
|
|
19
|
+
reference: [
|
|
20
|
+
`${PROFILE_ARGUMENTS} [--yes] [--github] [--public]`,
|
|
21
|
+
'developer',
|
|
22
|
+
['files', 'git', 'network', 'processes'],
|
|
23
|
+
],
|
|
20
24
|
},
|
|
21
25
|
init: {
|
|
22
26
|
script: 'scripts/init/init.ts',
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { hook_decision } from '#scripts/josh/hook-decision'
|
|
2
|
+
|
|
3
|
+
// Whether a run owes its end-of-run retrospective, single-sourced so `run:step` and `backlog:drive`
|
|
4
|
+
// never disagree on it (joshuafolkken/kit#2750): a drive that read "owed" where `run:step` read "not
|
|
5
|
+
// owed" handed back at every drain to a session that had nothing to run.
|
|
6
|
+
|
|
7
|
+
// The opt-in switch that gates the retrospective (joshuafolkken/kit#2370). It defaults off, so an unset
|
|
8
|
+
// variable owes no retrospective.
|
|
9
|
+
const RETROSPECTIVE_ENV_KEY = 'JOSH_RETROSPECTIVE'
|
|
10
|
+
|
|
11
|
+
interface RetrospectiveFacts {
|
|
12
|
+
is_retrospective_enabled: boolean
|
|
13
|
+
is_lane_child: boolean
|
|
14
|
+
is_retrospective_done: boolean
|
|
15
|
+
is_consumer: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function is_enabled(): boolean {
|
|
19
|
+
return hook_decision.is_switch_opt_in(RETROSPECTIVE_ENV_KEY)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// False when the switch is off, for a dispatched lane child (the batch runs one at its own end, never a
|
|
23
|
+
// child's), once it has run this invocation, and in a consumer checkout, where the kit-only command is
|
|
24
|
+
// refused. The switch is read first, so a run that has not opted in never consults the other three.
|
|
25
|
+
function is_owed(facts: RetrospectiveFacts): boolean {
|
|
26
|
+
if (!facts.is_retrospective_enabled) return false
|
|
27
|
+
|
|
28
|
+
return !facts.is_lane_child && !facts.is_retrospective_done && !facts.is_consumer
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const run_retrospective = { RETROSPECTIVE_ENV_KEY, is_enabled, is_owed }
|
|
32
|
+
|
|
33
|
+
export type { RetrospectiveFacts }
|
|
34
|
+
export { run_retrospective }
|
|
@@ -9,6 +9,7 @@ import { run_carry, type CarryRead } from './run-carry'
|
|
|
9
9
|
import { run_event_scope } from './run-event-scope'
|
|
10
10
|
import { run_event_stream } from './run-event-stream'
|
|
11
11
|
import { run_prep_cli } from './run-prep-cli'
|
|
12
|
+
import { run_retrospective } from './run-retrospective'
|
|
12
13
|
import { run_step, type StepInput } from './run-step'
|
|
13
14
|
|
|
14
15
|
// `josh run:step <N>` — print the run's next single action, computed from the event stream, the carry
|
|
@@ -22,10 +23,6 @@ const SUCCESS_EXIT_CODE = 0
|
|
|
22
23
|
const FAILURE_EXIT_CODE = 1
|
|
23
24
|
const ARGV_OFFSET = 2
|
|
24
25
|
const USAGE = 'Usage: josh run:step <issue-number>'
|
|
25
|
-
// The opt-in switch that gates the end-of-run retrospective (joshuafolkken/kit#2370). Read here rather
|
|
26
|
-
// than in `run-step.ts` so the position logic stays a pure function of its input; it defaults off, so an
|
|
27
|
-
// unset variable prints no retrospective step.
|
|
28
|
-
const RETROSPECTIVE_ENV_KEY = 'JOSH_RETROSPECTIVE'
|
|
29
26
|
|
|
30
27
|
interface RunReads {
|
|
31
28
|
carry_kind: CarryRead['kind']
|
|
@@ -83,7 +80,8 @@ async function gather(issue_number: string): Promise<StepInput> {
|
|
|
83
80
|
is_at_cut_cap: run_reads.is_at_cut_cap,
|
|
84
81
|
is_lane_child: lane_child_marker.is_child_of(process.cwd()),
|
|
85
82
|
is_consumer: doctor_consumer.is_kit_consumer(find_package_directory(process.cwd())),
|
|
86
|
-
|
|
83
|
+
// Read here rather than in `run-step.ts` so the position logic stays a pure function of its input.
|
|
84
|
+
is_retrospective_enabled: run_retrospective.is_enabled(),
|
|
87
85
|
has_changes: parts.has_changes,
|
|
88
86
|
has_completion_callback: agent_role_profile.has_completion_callback(),
|
|
89
87
|
}
|
package/scripts/run/run-step.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { git_stash } from '#scripts/git/git-stash'
|
|
|
2
2
|
import { latest_scope_cli } from '#scripts/version/latest-scope-cli'
|
|
3
3
|
import type { CarryRead } from './run-carry'
|
|
4
4
|
import { run_event_stream } from './run-event-stream'
|
|
5
|
+
import { run_retrospective } from './run-retrospective'
|
|
5
6
|
|
|
6
7
|
// `josh run:step <N>` — the run's next single action, computed from three mechanical inputs and nothing
|
|
7
8
|
// else (joshuafolkken/kit#2248). It is the child of epic #2166 that lifts the fold from an *event* to a
|
|
@@ -174,23 +175,10 @@ const EVENT_ACTIONS: Record<string, (issue_number: string) => StepAction> = {
|
|
|
174
175
|
// (joshuafolkken/kit#2297).
|
|
175
176
|
const PARENT_ONLY_EVENTS: ReadonlySet<string> = new Set([KIND.MERGE, KIND.OUTAGE])
|
|
176
177
|
|
|
177
|
-
// Whether a run at a winding-down position still owes its end-of-run retrospective
|
|
178
|
-
// (joshuafolkken/kit#2328) — false when the switch is off (the opt-in default, joshuafolkken/kit#2370),
|
|
179
|
-
// for a dispatched lane child, where the batch runs one at its own end and never a child's (the
|
|
180
|
-
// `release:scope` precedent), for a run whose retrospective has already run this invocation, and in a
|
|
181
|
-
// consumer checkout, where the kit-only command is refused. The switch is read first, so a run that has
|
|
182
|
-
// not opted in never consults the other three. The two positions that consult it are the drain and the
|
|
183
|
-
// stop; both read exactly this, so the exclusion set is single-sourced.
|
|
184
|
-
function is_retrospective_owed(input: StepInput): boolean {
|
|
185
|
-
if (!input.is_retrospective_enabled) return false
|
|
186
|
-
|
|
187
|
-
return !input.is_lane_child && !input.is_retrospective_done && !input.is_consumer
|
|
188
|
-
}
|
|
189
|
-
|
|
190
178
|
// A stopped run's last owed step is the end-of-run retrospective. When it is not owed the position stops
|
|
191
179
|
// with no command to run, which is what the position meant before the retrospective existed.
|
|
192
180
|
function stop_action(input: StepInput): StepAction {
|
|
193
|
-
return
|
|
181
|
+
return run_retrospective.is_owed(input) ? command(RETROSPECTIVE_COMMAND) : verdict(STOP)
|
|
194
182
|
}
|
|
195
183
|
|
|
196
184
|
// The drain fires the retrospective *before* the idle watch, not after it (joshuafolkken/kit#2335): the
|
|
@@ -199,7 +187,7 @@ function stop_action(input: StepInput): StepAction {
|
|
|
199
187
|
// run proceeds to that watch, which is a `WAIT` rather than a `STOP`: the backlog is empty but the run is
|
|
200
188
|
// not ending, and the real `STOP` follows when the watch expires with the retrospective already done.
|
|
201
189
|
function drain_action(input: StepInput): StepAction {
|
|
202
|
-
return
|
|
190
|
+
return run_retrospective.is_owed(input) ? command(RETROSPECTIVE_COMMAND) : verdict(WAIT)
|
|
203
191
|
}
|
|
204
192
|
|
|
205
193
|
// The hand-off a `backlogrun` parent without a completion callback takes right after dispatching
|