@joshuafolkken/kit 1.1471.0 → 1.1514.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +6 -6
- package/.claude/skills/epic-commands/SKILL.md +2 -14
- package/.claude/skills/epic-commands/epic-bundle.md +9 -5
- package/.claude/skills/epic-commands/execution-waves.md +1 -2
- package/.claude/skills/verify-ui/SKILL.md +1 -2
- package/.claude/skills/workflow-commands/SKILL.md +7 -9
- package/.claude/skills/workflow-commands/background-commands.md +3 -7
- package/.claude/skills/workflow-commands/backlogrun-child.md +3 -10
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +6 -15
- package/.claude/skills/workflow-commands/backlogrun-park.md +2 -8
- package/.claude/skills/workflow-commands/backlogrun-progress.md +10 -19
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +8 -8
- package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -10
- package/.claude/skills/workflow-commands/backlogrun.md +31 -50
- package/.claude/skills/workflow-commands/chain-rule.md +7 -6
- package/.claude/skills/workflow-commands/delegation.md +12 -8
- package/.claude/skills/workflow-commands/entry-sequence.md +1 -3
- package/.claude/skills/workflow-commands/followup.md +3 -6
- package/.claude/skills/workflow-commands/fullrun-steps.md +3 -5
- package/.claude/skills/workflow-commands/fullrun.md +4 -6
- package/.claude/skills/workflow-commands/halfrun.md +3 -7
- package/.claude/skills/workflow-commands/into-target.md +1 -2
- package/.claude/skills/workflow-commands/issue-comments.md +3 -3
- package/.claude/skills/workflow-commands/issue-scout.md +3 -2
- package/.claude/skills/workflow-commands/latest-gate.md +2 -6
- package/.claude/skills/workflow-commands/needs-human-review.md +2 -5
- package/.claude/skills/workflow-commands/observation-filing.md +0 -14
- package/.claude/skills/workflow-commands/observation-ledger.md +4 -9
- package/.claude/skills/workflow-commands/pre-gate-cut.md +11 -16
- package/.claude/skills/workflow-commands/prerequisite.md +1 -2
- package/.claude/skills/workflow-commands/progress-watcher.md +2 -7
- package/.claude/skills/workflow-commands/prrun.md +3 -5
- package/.claude/skills/workflow-commands/retrospective.md +4 -4
- package/.claude/skills/workflow-commands/split-assessment.md +1 -3
- package/.claude/skills/workflow-commands/target-repository.md +1 -2
- package/.claude/skills/workflow-commands/working-tree-hold.md +2 -4
- package/.codex/hooks.json +6 -6
- package/.github/actions/setup-node/action.yml +1 -1
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/publish.yml +2 -2
- package/CLAUDE.md +1 -1
- package/cspell/index.yaml +7 -0
- package/dist/CLAUDE.md +1 -1
- package/dist/commands/document-section-cli.js +67 -34
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/chunk-4KKD2UTK.js +66 -0
- package/dist/hooks/{chunk-J47HU5H5.js → chunk-6TS2JD6D.js} +3 -3
- package/dist/hooks/chunk-7DSBJVFH.js +50 -0
- package/dist/hooks/{chunk-YHBZAJSV.js → chunk-A65Y4YOR.js} +3 -3
- package/dist/hooks/{chunk-E7OIKX5Z.js → chunk-APUO6U4J.js} +85 -7
- package/dist/hooks/{chunk-IX6TTTI6.js → chunk-EFXE6C37.js} +8 -22
- package/dist/hooks/chunk-GP4IEC4O.js +20 -0
- package/dist/hooks/{chunk-SHGPANFB.js → chunk-JP45SOCJ.js} +3 -3
- package/dist/hooks/{chunk-K4EOONYM.js → chunk-LAOV7NLM.js} +1 -1
- package/dist/hooks/chunk-M4HGZV7U.js +83 -0
- package/dist/hooks/{chunk-KC5AK2GF.js → chunk-O6GDRNJB.js} +3 -3
- package/dist/hooks/{chunk-YYPI7SJZ.js → chunk-PF2NZL2A.js} +5 -5
- package/dist/hooks/{chunk-H5HVWYXK.js → chunk-PTICX4OL.js} +3 -4
- package/dist/hooks/{chunk-IIGFYXKC.js → chunk-RT27QRJW.js} +581 -1438
- package/dist/hooks/{chunk-43ZHT22P.js → chunk-SYOTDYCI.js} +1127 -1089
- package/dist/hooks/{chunk-AS74OU3D.js → chunk-TNUMY6NL.js} +1507 -1336
- package/dist/hooks/chunk-UPUZF3AN.js +279 -0
- package/dist/hooks/{chunk-EUMACUK2.js → chunk-WK6SMBIC.js} +1948 -806
- package/dist/hooks/codex-hook-adapter.js +17 -14
- package/dist/hooks/duplicate-read-guard.js +7 -7
- package/dist/hooks/format-edited.js +5 -4
- package/dist/hooks/inputs.json +317 -264
- package/dist/hooks/investigation-guard.js +8 -11
- package/dist/hooks/pretool-guard.js +16 -13
- package/dist/hooks/session-lang.js +370 -25
- package/dist/hooks/stop-guard.js +2576 -176
- package/dist/josh.js +2124 -277
- package/lefthook/base.yml +5 -3
- package/package.json +12 -10
- package/prompts/collaboration-workflow/file-edits.md +1 -3
- package/prompts/collaboration-workflow/gh-rest.md +1 -2
- package/prompts/collaboration-workflow/issue-citation.md +2 -4
- package/prompts/collaboration-workflow/issue-template.md +10 -2
- package/prompts/collaboration-workflow/lane-instructions.md +35 -0
- package/prompts/collaboration-workflow/operating-rules.md +2 -6
- package/prompts/collaboration-workflow/output-bounds.md +3 -3
- package/prompts/collaboration-workflow/overview.md +1 -1
- package/prompts/collaboration-workflow/principles.md +14 -8
- package/prompts/collaboration-workflow/report-format.md +9 -0
- package/prompts/collaboration-workflow/residency.md +12 -8
- package/prompts/collaboration-workflow/rule-delivery.md +3 -11
- package/prompts/collaboration-workflow/shell-body.md +6 -6
- package/prompts/collaboration-workflow/turn-batching.md +5 -5
- package/prompts/collaboration-workflow/upstream-interrupt.md +2 -2
- package/prompts/collaboration-workflow/wip-cap.md +6 -6
- package/prompts/collaboration-workflow.md +1 -0
- package/prompts/review.md +4 -5
- package/prompts/testing-guide.md +2 -2
- package/scripts/agent/agent-argv.ts +20 -4
- package/scripts/agent/agent-role-profile.ts +101 -45
- package/scripts/agent/agent-safe-text.ts +60 -0
- package/scripts/agent/claude-agent-argv.ts +14 -0
- package/scripts/agent/codex-agent-argv.ts +2 -0
- package/scripts/agent/lane-instructions.ts +144 -0
- package/scripts/backlog/backlog-budget.ts +4 -4
- package/scripts/backlog/backlog-drive-named.ts +1 -1
- package/scripts/backlog/backlog-drive-restore.ts +9 -1
- package/scripts/backlog/backlog-occupied.ts +56 -0
- package/scripts/backlog/backlog-ready.ts +47 -10
- package/scripts/delegation/duplicate-read-guard.ts +2 -2
- package/scripts/delegation/investigation-guard.ts +15 -65
- package/scripts/delegation/investigation-reads.ts +0 -1
- package/scripts/document/document-byte-budget.ts +6 -5
- package/scripts/document/document-section-cli.ts +17 -5
- package/scripts/document/entry-read-budget.ts +2 -2
- package/scripts/document/read-set-cli.ts +4 -1
- package/scripts/epic/epic-audit-orphans.ts +5 -8
- package/scripts/epic/epic-audit.ts +52 -1
- package/scripts/epic/epic-bundle-evidence.ts +49 -26
- package/scripts/epic/epic-bundle-referenced.ts +14 -3
- package/scripts/epic/epic-bundle.ts +17 -9
- package/scripts/followup/git-followup-workflow.ts +19 -1
- package/scripts/gate/check-inputs.ts +129 -0
- package/scripts/gate/check-reuse.ts +162 -0
- package/scripts/gate/core-budget.ts +93 -44
- package/scripts/gate/core-wait.ts +130 -0
- package/scripts/gate/free-cores.ts +49 -0
- package/scripts/gate/gate-ledger.ts +17 -3
- package/scripts/gate/gate-plan.ts +23 -1
- package/scripts/gate/gate-report.ts +15 -2
- package/scripts/gate/gate-run-steps.ts +150 -0
- package/scripts/gate/held-mark.ts +55 -0
- package/scripts/gate/machine-capacity.ts +70 -11
- package/scripts/gate/pre-commit-type-check.ts +14 -1
- package/scripts/gate/verification-gate.ts +58 -125
- package/scripts/gh/git-gh-issue-rest.ts +1 -0
- package/scripts/gh/git-gh-issue-write.ts +24 -2
- package/scripts/gh/git-pr.ts +13 -1
- package/scripts/git/main-merge-regenerate.ts +78 -0
- package/scripts/git/main-merge.ts +14 -5
- package/scripts/hooks/batch-guard.ts +5 -8
- package/scripts/hooks/pre-commit-lint.ts +91 -0
- package/scripts/hooks/pre-push-unit.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +27 -52
- package/scripts/hooks/reserved-run.ts +1 -0
- package/scripts/hooks/run-hook.sh +10 -12
- package/scripts/hooks/stop-actions.ts +204 -0
- package/scripts/hooks/stop-guard.ts +62 -21
- package/scripts/init/hook-launch.ts +12 -7
- package/scripts/issue/expected-effect-lint.ts +59 -0
- package/scripts/issue/issue-cite-cli.ts +35 -15
- package/scripts/issue/issue-classification.ts +1 -1
- package/scripts/issue/issue-lint-cli.ts +5 -3
- package/scripts/issue/issue-scout-cli.ts +19 -14
- package/scripts/issue/markdown-section.ts +19 -5
- package/scripts/josh/command-usage.ts +110 -0
- package/scripts/josh/josh-command-map.ts +2 -0
- package/scripts/josh/josh-command-types.ts +55 -9
- package/scripts/josh/josh-commands-ai.ts +13 -0
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-hooks.ts +6 -0
- package/scripts/josh/josh-commands-lane.ts +10 -1
- package/scripts/josh/josh-commands-workflow.ts +5 -0
- package/scripts/josh/josh-logic.ts +6 -15
- package/scripts/josh/josh-usage-automation.ts +121 -0
- package/scripts/josh/josh-usage-backlog.ts +180 -0
- package/scripts/josh/josh-usage-developer.ts +230 -0
- package/scripts/josh/josh-usage-hooks.ts +38 -0
- package/scripts/josh/josh-usage-issue.ts +205 -0
- package/scripts/josh/josh-usage-review.ts +101 -0
- package/scripts/josh/josh-usage-run.ts +252 -0
- package/scripts/josh/josh-usage-session.ts +238 -0
- package/scripts/josh/josh-usage-workflow.ts +212 -0
- package/scripts/josh/josh.ts +14 -5
- package/scripts/josh/package-directory.ts +20 -0
- package/scripts/lane/lane-capacity.ts +8 -2
- package/scripts/lane/lane-check-stats.ts +56 -0
- package/scripts/lane/lane-child-invocation.ts +35 -4
- package/scripts/lane/lane-dispatch.ts +3 -3
- package/scripts/lane/lane-guard-policy.ts +2 -2
- package/scripts/lane/lane-handoff.ts +8 -1
- package/scripts/lane/lane-install.ts +12 -4
- package/scripts/lane/lane-ledger.ts +43 -1
- package/scripts/lane/lane-limit-cli.ts +8 -9
- package/scripts/lane/lane-measure-cli.ts +2 -0
- package/scripts/lane/lane-preserve.ts +29 -1
- package/scripts/lane/lane-registry.ts +9 -7
- package/scripts/lane/lane-restart.ts +5 -26
- package/scripts/lane/lane-session-action.ts +146 -0
- package/scripts/lane/lane-session-broker.ts +57 -138
- package/scripts/lane/lane-withdraw-cli.ts +71 -0
- package/scripts/lane/lane-withdraw.ts +182 -0
- package/scripts/lint/format.ts +4 -4
- package/scripts/lint/lint-parallel.ts +10 -3
- package/scripts/lint/lint-related-scope.ts +4 -1
- package/scripts/notify/telegram-test.ts +10 -5
- package/scripts/report/report-format-reference.ts +2 -0
- package/scripts/review/acceptance-map-check.ts +61 -0
- package/scripts/review/acceptance-map.ts +205 -0
- package/scripts/review/review-brief-cli.ts +1 -1
- package/scripts/rules/agent-model.ts +59 -0
- package/scripts/rules/delivered-rules.ts +11 -2
- package/scripts/rules/filing-cap.ts +1 -0
- package/scripts/rules/firing-point.ts +6 -5
- package/scripts/rules/guard-remedy.ts +117 -0
- package/scripts/rules/issue-comments.ts +67 -6
- package/scripts/rules/last-prompt.ts +50 -5
- package/scripts/rules/oracle-list-cli.ts +7 -0
- package/scripts/rules/piped-verification.ts +3 -1
- package/scripts/rules/rule-list.ts +4 -1
- package/scripts/rules/rule-registry-rows.ts +10 -0
- package/scripts/rules/rule-registry.ts +7 -4
- package/scripts/rules/rule-rewrite.ts +114 -0
- package/scripts/rules/rule-value.ts +7 -1
- package/scripts/rules/shell-segments.ts +15 -0
- package/scripts/rules/stop-decision.ts +77 -0
- package/scripts/rules/stop-rules.ts +147 -47
- package/scripts/rules/tail-commands.ts +15 -1
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-closed.ts +8 -2
- package/scripts/run/board/run-board-github.ts +9 -1
- package/scripts/run/board/run-board-header.ts +4 -1
- package/scripts/run/board/run-board-labels.ts +2 -0
- package/scripts/run/board/run-board-layout.ts +1 -1
- package/scripts/run/board/run-board-read.ts +16 -0
- package/scripts/run/board/run-board-reason.ts +10 -1
- package/scripts/run/board/run-board-render-legend.ts +1 -0
- package/scripts/run/board/run-board-render-slot.ts +1 -1
- package/scripts/run/board/run-board-render-waits.ts +41 -0
- package/scripts/run/board/run-board-render.ts +6 -1
- package/scripts/run/board/run-board-row-yield.ts +1 -0
- package/scripts/run/board/run-board-state.ts +5 -0
- package/scripts/run/board/run-board-status.ts +72 -21
- package/scripts/run/board/run-board-tick.ts +5 -1
- package/scripts/run/cut/run-cut-cli.ts +3 -2
- package/scripts/run/cut/run-cut-report.ts +16 -0
- package/scripts/run/detached-launch.ts +23 -4
- package/scripts/run/entry/run-entry-cli.ts +8 -1
- package/scripts/run/event/run-event-render.ts +1 -0
- package/scripts/run/event/run-event-scope.ts +19 -1
- package/scripts/run/event/run-event-stream-emit.ts +13 -6
- package/scripts/run/event/run-event-stream.ts +4 -0
- package/scripts/run/hold/run-hold-session.ts +46 -0
- package/scripts/run/hold/run-hold.ts +7 -1
- package/scripts/run/launch-priority.ts +32 -0
- package/scripts/run/merge/run-merge-cli.ts +16 -65
- package/scripts/run/merge/run-merge-signals.ts +99 -0
- package/scripts/run/merge/run-merge-steps.ts +11 -1
- package/scripts/run/merge/run-merge.ts +21 -9
- package/scripts/run/run-liveness.ts +1 -1
- package/scripts/run/run-poll-hook.ts +157 -0
- package/scripts/run/run-step-cli.ts +22 -15
- package/scripts/run/run-step-section.ts +85 -0
- package/scripts/run/ship/run-ship-cli.ts +5 -12
- package/scripts/run/ship/run-ship-detach.ts +9 -0
- package/scripts/run/ship/run-ship-next.ts +10 -2
- package/scripts/run/ship/run-ship-overlap.ts +195 -0
- package/scripts/run/ship/run-ship-pre-detach.ts +33 -15
- package/scripts/run/ship/run-ship-preflight.ts +28 -2
- package/scripts/run/ship/run-ship-return.ts +31 -7
- package/scripts/run/ship/run-ship-review-steps.ts +12 -10
- package/scripts/run/ship/run-ship-steps.ts +1 -1
- package/scripts/run/ship/run-ship-stop-context.ts +206 -0
- package/scripts/run/ship/run-ship-sync.ts +14 -6
- package/scripts/test/test-e2e-guard.ts +30 -5
- package/scripts/test/test-unit-guard.ts +16 -1
- package/scripts/test/unit-worker-share.ts +26 -4
- package/scripts/time-runtime/time-shell.ts +29 -4
- package/scripts/time-runtime/time-spans.ts +1 -0
- package/scripts/time-runtime/time-tool-call.ts +7 -0
- package/templates/gitignore +6 -2
- package/dist/hooks/chunk-K4LMBQC2.js +0 -68
|
@@ -5,12 +5,14 @@ const SETTLED_KINDS: ReadonlySet<string> = new Set([
|
|
|
5
5
|
run_event_stream.EVENT_KIND.MERGE,
|
|
6
6
|
run_event_stream.EVENT_KIND.PARK,
|
|
7
7
|
run_event_stream.EVENT_KIND.SPLIT,
|
|
8
|
+
run_event_stream.EVENT_KIND.WITHDRAW,
|
|
8
9
|
run_event_stream.EVENT_KIND.OUTAGE,
|
|
9
10
|
])
|
|
10
11
|
const DONE_KINDS: ReadonlySet<string> = new Set([
|
|
11
12
|
run_event_stream.EVENT_KIND.MERGE,
|
|
12
13
|
run_event_stream.EVENT_KIND.PARK,
|
|
13
14
|
run_event_stream.EVENT_KIND.SPLIT,
|
|
15
|
+
run_event_stream.EVENT_KIND.WITHDRAW,
|
|
14
16
|
])
|
|
15
17
|
|
|
16
18
|
function issue_of(event: RunEvent): string | undefined {
|
|
@@ -82,4 +84,10 @@ function restore(
|
|
|
82
84
|
)
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
export const backlog_drive_restore = {
|
|
87
|
+
export const backlog_drive_restore = {
|
|
88
|
+
active_issues,
|
|
89
|
+
mentioned_issues,
|
|
90
|
+
restore,
|
|
91
|
+
settled_issues,
|
|
92
|
+
parked_issues,
|
|
93
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { LaneInfo } from '#scripts/lane/lane-registry'
|
|
2
|
+
import type { RunEvent } from '#scripts/run/event/run-event-stream'
|
|
3
|
+
import { run_invocation } from '#scripts/run/run-invocation'
|
|
4
|
+
import { backlog_drive_restore } from './backlog-drive-restore'
|
|
5
|
+
|
|
6
|
+
// The open lanes that hold a place under the lane limit — the one count `lane:limit`, the drive and
|
|
7
|
+
// `run:board` all subtract from it.
|
|
8
|
+
//
|
|
9
|
+
// **A lane nobody will come back to holds no place.** A `--only` run works its named list and never
|
|
10
|
+
// touches another lane, so a lane an earlier run left with no child in it would fill the limit for the
|
|
11
|
+
// whole run, and the run's own issues would wait on it for good (joshuafolkken/kit#3967). Under a `--only`
|
|
12
|
+
// run such a lane is not counted; its tree and branch stay, for the next run that names its issue.
|
|
13
|
+
//
|
|
14
|
+
// **Everything else keeps its place, so the count errs toward fewer free lanes.** A run that drains
|
|
15
|
+
// the pool may offer any open lane's issue again, a lane whose child still works is busy whoever owns
|
|
16
|
+
// it, and with no run there is nobody to judge ownership for. The read stays local — no issue label is
|
|
17
|
+
// read — because the count gates the network read behind it (`backlog-ready.ts`).
|
|
18
|
+
|
|
19
|
+
interface OccupiedRead {
|
|
20
|
+
// The live run's invocation, `undefined` when no run is live here.
|
|
21
|
+
invocation: string | undefined
|
|
22
|
+
// The live run's own events, for the issues it launched or settled; `undefined` where its scope
|
|
23
|
+
// could not be told, which leaves every open lane counted.
|
|
24
|
+
events: ReadonlyArray<RunEvent> | undefined
|
|
25
|
+
// The issues whose lane has no child working in it; read only for a `--only` run.
|
|
26
|
+
idle: (lanes: ReadonlyArray<LaneInfo>) => ReadonlyArray<string>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The issues a `--only` run owns — those it names and those it launched or settled — or `undefined`
|
|
30
|
+
// where there is no such run, or its events could not be scoped to it.
|
|
31
|
+
function owned_issues(read: OccupiedRead): ReadonlySet<string> | undefined {
|
|
32
|
+
const { invocation, events } = read
|
|
33
|
+
const is_only = invocation !== undefined && run_invocation.has_only(invocation)
|
|
34
|
+
|
|
35
|
+
if (!is_only || events === undefined) return undefined
|
|
36
|
+
|
|
37
|
+
const named = (run_invocation.issue_numbers(invocation) ?? []).map(String)
|
|
38
|
+
|
|
39
|
+
return new Set([...named, ...backlog_drive_restore.mentioned_issues(events)])
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function occupied_lanes(lanes: ReadonlyArray<LaneInfo>, read: OccupiedRead): Array<LaneInfo> {
|
|
43
|
+
const live = lanes.filter((lane) => !lane.is_stranded)
|
|
44
|
+
const owned = owned_issues(read)
|
|
45
|
+
|
|
46
|
+
if (owned === undefined) return live
|
|
47
|
+
|
|
48
|
+
const idle = new Set(read.idle(live))
|
|
49
|
+
|
|
50
|
+
return live.filter((lane) => owned.has(lane.issue) || !idle.has(lane.issue))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const backlog_occupied = { occupied_lanes }
|
|
54
|
+
|
|
55
|
+
export { backlog_occupied }
|
|
56
|
+
export type { OccupiedRead }
|
|
@@ -3,9 +3,12 @@ import { issue_cite } from '#scripts/issue/issue-cite'
|
|
|
3
3
|
import { josh_command, type JoshResult } from '#scripts/josh/josh-run'
|
|
4
4
|
import { lane_capacity } from '#scripts/lane/lane-capacity'
|
|
5
5
|
import { lane_registry, type LaneInfo } from '#scripts/lane/lane-registry'
|
|
6
|
+
import { lane_spent } from '#scripts/lane/lane-spent'
|
|
6
7
|
import type { RunCarry } from '#scripts/run/carry/run-carry'
|
|
8
|
+
import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
|
|
7
9
|
import { run_headless } from '#scripts/run/run-headless'
|
|
8
10
|
import { run_invocation } from '#scripts/run/run-invocation'
|
|
11
|
+
import { backlog_occupied, type OccupiedRead } from './backlog-occupied'
|
|
9
12
|
import { backlog_stalled } from './backlog-stalled'
|
|
10
13
|
|
|
11
14
|
// The ready line reports runnable work to the driving backlogrun parent on a watcher wake.
|
|
@@ -18,14 +21,46 @@ interface ReadyReading {
|
|
|
18
21
|
free_lanes: number
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
// How many open lanes hold a place under the limit, and how many hold a port seat.
|
|
25
|
+
interface LaneUse {
|
|
26
|
+
occupied: number
|
|
27
|
+
seated: number
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
interface ReadyPorts {
|
|
22
31
|
free_lane_count: () => Promise<number>
|
|
23
32
|
ready_issues: () => Promise<ReadonlyArray<string>>
|
|
24
33
|
}
|
|
25
34
|
|
|
26
|
-
//
|
|
27
|
-
async function
|
|
28
|
-
|
|
35
|
+
// What decides which open lanes hold a place, read from this checkout's live run.
|
|
36
|
+
async function occupied_read(): Promise<OccupiedRead> {
|
|
37
|
+
const carry = await run_headless.current_carry()
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
invocation: carry?.invocation,
|
|
41
|
+
events: carry === undefined ? [] : await run_event_stream_emit.scoped_current_events(),
|
|
42
|
+
idle: lane_spent.idle_lanes,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The lanes that hold a place under the limit (`backlog-occupied.ts`).
|
|
47
|
+
async function occupied_lanes(lanes?: ReadonlyArray<LaneInfo>): Promise<Array<LaneInfo>> {
|
|
48
|
+
const open = lanes ?? (await lane_registry.list_lanes())
|
|
49
|
+
|
|
50
|
+
return backlog_occupied.occupied_lanes(open, await occupied_read())
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The open lanes as the free count reads them: those that hold a place under the limit, and every
|
|
54
|
+
// open one, since an uncounted lane still holds its port seat.
|
|
55
|
+
async function lane_use(lanes?: ReadonlyArray<LaneInfo>): Promise<LaneUse> {
|
|
56
|
+
const open = lanes ?? (await lane_registry.list_lanes())
|
|
57
|
+
const occupied = await occupied_lanes(open)
|
|
58
|
+
|
|
59
|
+
return { occupied: occupied.length, seated: open.filter((lane) => !lane.is_stranded).length }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function free_of(limit: number, use: LaneUse): number {
|
|
63
|
+
return lane_capacity.free_lanes(limit, use.occupied, use.seated)
|
|
29
64
|
}
|
|
30
65
|
|
|
31
66
|
// Free lanes beside `lanes`, `undefined` where the limit could not be read: the count `lane:limit`
|
|
@@ -35,11 +70,11 @@ async function free_lanes_beside(lanes: ReadonlyArray<LaneInfo>): Promise<number
|
|
|
35
70
|
|
|
36
71
|
if (limit.kind !== 'limit') return undefined
|
|
37
72
|
|
|
38
|
-
return
|
|
73
|
+
return free_of(limit.limit, await lane_use(lanes))
|
|
39
74
|
}
|
|
40
75
|
|
|
41
76
|
async function free_under(limit: number): Promise<number> {
|
|
42
|
-
return
|
|
77
|
+
return free_of(limit, await lane_use())
|
|
43
78
|
}
|
|
44
79
|
|
|
45
80
|
// Free lanes right now: the limit less the live lanes. An unreadable limit reads as no free lane — the
|
|
@@ -48,9 +83,10 @@ async function free_under(limit: number): Promise<number> {
|
|
|
48
83
|
//
|
|
49
84
|
// **Occupancy is the local worktree count, not the dispatch budget's busy read.** `epic-lane-offer`
|
|
50
85
|
// measures occupancy with `epic_busy.occupied_lanes`, a network read of in-progress issue labels; this
|
|
51
|
-
// counts
|
|
52
|
-
// (`backlog:next`) is gated behind a free lane, so a network
|
|
53
|
-
// two can diverge, but only toward over-counting live lanes →
|
|
86
|
+
// counts the worktrees that hold a place instead (`backlog-occupied.ts`), deliberately — it runs on the
|
|
87
|
+
// cheap-first path where the costly read (`backlog:next`) is gated behind a free lane, so a network
|
|
88
|
+
// read here would defeat that design. The two can diverge, but only toward over-counting live lanes →
|
|
89
|
+
// fewer free lanes, the safe direction.
|
|
54
90
|
async function free_lane_count(): Promise<number> {
|
|
55
91
|
return (await free_lanes_beside(await lane_registry.list_lanes())) ?? NO_FREE
|
|
56
92
|
}
|
|
@@ -172,7 +208,8 @@ const backlog_ready = {
|
|
|
172
208
|
print_offer_hint,
|
|
173
209
|
free_lane_count,
|
|
174
210
|
free_lanes_beside,
|
|
175
|
-
|
|
211
|
+
lane_use,
|
|
212
|
+
occupied_lanes,
|
|
176
213
|
drive_free_lane_count,
|
|
177
214
|
drains_pool,
|
|
178
215
|
print_ready_line,
|
|
@@ -182,4 +219,4 @@ const backlog_ready = {
|
|
|
182
219
|
}
|
|
183
220
|
|
|
184
221
|
export { backlog_ready }
|
|
185
|
-
export type { ReadyPorts, ReadyReading }
|
|
222
|
+
export type { LaneUse, ReadyPorts, ReadyReading }
|
|
@@ -176,8 +176,8 @@ function is_candidate(call: GuardedCall): boolean {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
// A refusable call is refused only where the mode is `refuse`. A lane child is `notice`, so it reaches
|
|
179
|
-
// here and is withheld the refusal — the notice branch carries the guidance instead,
|
|
180
|
-
//
|
|
179
|
+
// here and is withheld the refusal — the notice branch carries the guidance instead, for the reason its
|
|
180
|
+
// `because` in `lane-guard-policy.ts` records.
|
|
181
181
|
function should_block_here(tail: string, call: GuardedCall, refused_at_ms: number): boolean {
|
|
182
182
|
if (duplicate_mode() !== 'refuse') return false
|
|
183
183
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { text } from 'node:stream/consumers'
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { agent_session_role } from '#scripts/agent/agent-session-role'
|
|
4
5
|
import { hook_decision, type GuardRun } from '#scripts/josh/hook-decision'
|
|
5
|
-
import { lane_guard_policy
|
|
6
|
+
import { lane_guard_policy } from '#scripts/lane/lane-guard-policy'
|
|
6
7
|
import type { GuardedCall } from '#scripts/time-runtime/time-batch-guard'
|
|
7
8
|
import { investigation_reads } from './investigation-reads'
|
|
8
9
|
|
|
@@ -21,91 +22,42 @@ import { investigation_reads } from './investigation-reads'
|
|
|
21
22
|
// file — debugging the guard itself, or a session whose reading really is all edit targets.
|
|
22
23
|
const SWITCH_ENV_KEY = 'JOSH_INVESTIGATION_GUARD'
|
|
23
24
|
|
|
24
|
-
// This guard's refusal record, and the notice's own record — distinct so a lane-child notice never
|
|
25
|
-
// spends the refusal's stamp, the same split the batching guard keeps.
|
|
26
25
|
const STAMP_PREFIX = 'josh-investigation-guard-'
|
|
27
|
-
const NOTICE_STAMP_PREFIX = 'josh-investigation-guard-notice-'
|
|
28
26
|
|
|
29
|
-
// **How the guard behaves for the session in hand, decided from the one-place enumeration**.
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
function investigation_mode(): LaneGuardMode {
|
|
37
|
-
return lane_guard_policy.mode_here('investigation')
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// A refusable call is refused only where the mode is `refuse` — the main line. A lane child is
|
|
41
|
-
// `notice`, so it reaches the notice branch and is withheld the refusal that would end its headless
|
|
42
|
-
// turn; an `off` some future enumeration sets stays silent here too.
|
|
27
|
+
// **How the guard behaves for the session in hand, decided from the one-place enumeration**. It is
|
|
28
|
+
// `refuse` in a dispatched lane child too (joshuafolkken/kit#3927): a hook denial returns to the model
|
|
29
|
+
// as guidance, and the notice it replaced was measured not to move the reading. It wraps
|
|
30
|
+
// `investigation_reads.should_block` here rather than inside it so the pure counting rule stays free of
|
|
31
|
+
// process state and its own suite keeps testing it directly. A reviewer session stands down: reading
|
|
32
|
+
// the diff's files is its whole task, and it carries the lane's marker, so it would otherwise be refused
|
|
33
|
+
// like the child it reviews — the stand-down `implementation-cut.ts` takes for the same session.
|
|
43
34
|
function should_block(
|
|
44
35
|
tail: string,
|
|
45
36
|
call: GuardedCall,
|
|
46
37
|
refused_at_ms: number,
|
|
47
38
|
run?: GuardRun,
|
|
48
39
|
): boolean {
|
|
49
|
-
if (
|
|
40
|
+
if (
|
|
41
|
+
agent_session_role.is_reviewer() ||
|
|
42
|
+
lane_guard_policy.mode_here('investigation') !== 'refuse'
|
|
43
|
+
) {
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
50
46
|
|
|
51
47
|
return investigation_reads.should_block(tail, call, refused_at_ms, run)
|
|
52
48
|
}
|
|
53
49
|
|
|
54
|
-
// The lane-child notice: the same threshold, delivered without a `permissionDecision`. It
|
|
55
|
-
// reads the notify stamp's own record, so a notice never spends the refusal's stamp and re-arms on the
|
|
56
|
-
// next accumulation exactly as the refusal does.
|
|
57
|
-
function should_notify(
|
|
58
|
-
tail: string,
|
|
59
|
-
call: GuardedCall,
|
|
60
|
-
notified_at_ms: number,
|
|
61
|
-
run?: GuardRun,
|
|
62
|
-
): boolean {
|
|
63
|
-
if (investigation_mode() !== 'notice') return false
|
|
64
|
-
|
|
65
|
-
return investigation_reads.should_block(tail, call, notified_at_ms, run)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// **The lane-child counterpart of REASON, delivered as a notice rather than a refusal**.
|
|
69
|
-
// A dispatched lane child ends its turn on a denial, so
|
|
70
|
-
// the same guidance is carried without a `permissionDecision`: the read proceeds and the child is nudged
|
|
71
|
-
// to delegate rather than killed. It carries no ⛔, so a person watching reads it as advice.
|
|
72
|
-
const NOTICE = `💡 investigation: files read and not edited, or search turns, since the last delegated unit reached the threshold; the read proceeds. Run \`pnpm josh delegate investigation\`, then dispatch the \`${investigation_reads.INVESTIGATOR_AGENT}\` agent (\`kit:${investigation_reads.INVESTIGATOR_AGENT}\` in a consumer); it returns \`file:line\` citations, never the file text. Keep main-line reads to files you will edit.`
|
|
73
|
-
|
|
74
|
-
// The notice as it reaches the model: the base guidance with the concrete unedited reads named, so a
|
|
75
|
-
// lane child is shown *which* reads to send to a unit rather than only told to delegate. An empty
|
|
76
|
-
// set names nothing and the notice falls back to its guidance alone. The call is part of the notify
|
|
77
|
-
// contract but this guard's wording depends on the tail alone.
|
|
78
|
-
function notice_text(_call: GuardedCall, tail: string): string {
|
|
79
|
-
const pending = investigation_reads
|
|
80
|
-
.tally_of(tail)
|
|
81
|
-
.pending.map((absolute) => investigation_reads.repository_relative(absolute))
|
|
82
|
-
|
|
83
|
-
return pending.length === 0
|
|
84
|
-
? NOTICE
|
|
85
|
-
: `${NOTICE} Read and not edited so far: ${pending.join(' ')}.`
|
|
86
|
-
}
|
|
87
|
-
|
|
88
50
|
const GUARD = hook_decision.create_transcript_guard({
|
|
89
51
|
prefix: STAMP_PREFIX,
|
|
90
52
|
switch_key: SWITCH_ENV_KEY,
|
|
91
53
|
is_candidate: investigation_reads.is_refusable_call,
|
|
92
54
|
should_block,
|
|
93
55
|
reason: investigation_reads.REASON,
|
|
94
|
-
// The lane-child notice fires where the refusal is withheld, on a record of its own so it
|
|
95
|
-
// never spends the refusal's stamp.
|
|
96
|
-
notify: {
|
|
97
|
-
prefix: NOTICE_STAMP_PREFIX,
|
|
98
|
-
should_notify,
|
|
99
|
-
text: notice_text,
|
|
100
|
-
},
|
|
101
56
|
})
|
|
102
57
|
|
|
103
58
|
const investigation_refusal = GUARD.refusal
|
|
104
59
|
const investigation_outcome = GUARD.outcome
|
|
105
60
|
const { is_enabled, refusal_path } = GUARD
|
|
106
|
-
// The notice's stamp path, exposed the way `refusal_path` is so a test can clean it apart from the
|
|
107
|
-
// refusal's record.
|
|
108
|
-
const investigation_notice_path = hook_decision.create_refusal_stamp(NOTICE_STAMP_PREFIX).path
|
|
109
61
|
const { deny_envelope, load_environment_file, DISABLED_VALUES } = hook_decision
|
|
110
62
|
|
|
111
63
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
@@ -115,13 +67,11 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
|
115
67
|
|
|
116
68
|
export {
|
|
117
69
|
deny_envelope,
|
|
118
|
-
investigation_notice_path,
|
|
119
70
|
investigation_outcome,
|
|
120
71
|
investigation_refusal,
|
|
121
72
|
is_enabled,
|
|
122
73
|
load_environment_file,
|
|
123
74
|
refusal_path,
|
|
124
75
|
DISABLED_VALUES,
|
|
125
|
-
NOTICE,
|
|
126
76
|
SWITCH_ENV_KEY,
|
|
127
77
|
}
|
|
@@ -73,18 +73,19 @@ const DOCUMENT_BYTE_BUDGET: ReadonlyArray<DocumentBudget> = [
|
|
|
73
73
|
{ path: '.claude/skills/workflow-commands/prerequisite.md', bytes: 8192 },
|
|
74
74
|
{ path: '.claude/skills/workflow-commands/target-repository.md', bytes: 4096 },
|
|
75
75
|
{ path: '.claude/skills/workflow-commands/working-tree-hold.md', bytes: 8192 },
|
|
76
|
-
{ path: 'docs/josh-commands-automation.md', bytes:
|
|
77
|
-
{ path: 'docs/josh-commands-run.md', bytes:
|
|
78
|
-
{ path: 'docs/josh-commands-backlog.md', bytes:
|
|
79
|
-
{ path: 'docs/josh-commands.md', bytes:
|
|
76
|
+
{ path: 'docs/josh-commands-automation.md', bytes: 49_152 },
|
|
77
|
+
{ path: 'docs/josh-commands-run.md', bytes: 65_536 },
|
|
78
|
+
{ path: 'docs/josh-commands-backlog.md', bytes: 53_248 },
|
|
79
|
+
{ path: 'docs/josh-commands.md', bytes: 49_152 },
|
|
80
80
|
{ path: 'docs/labels-and-run-states.md', bytes: 8192 },
|
|
81
81
|
{ path: 'prompts/coding-standards.md', bytes: 4096 },
|
|
82
82
|
{ path: 'prompts/collaboration-workflow.md', bytes: 4096 },
|
|
83
|
-
{ path: 'prompts/collaboration-workflow/file-edits.md', bytes:
|
|
83
|
+
{ path: 'prompts/collaboration-workflow/file-edits.md', bytes: 4096 },
|
|
84
84
|
{ path: 'prompts/collaboration-workflow/gh-rest.md', bytes: 4096 },
|
|
85
85
|
{ path: 'prompts/collaboration-workflow/glossary.md', bytes: 4096 },
|
|
86
86
|
{ path: 'prompts/collaboration-workflow/issue-citation.md', bytes: 4096 },
|
|
87
87
|
{ path: 'prompts/collaboration-workflow/issue-template.md', bytes: 8192 },
|
|
88
|
+
{ path: 'prompts/collaboration-workflow/lane-instructions.md', bytes: 4096 },
|
|
88
89
|
{ path: 'prompts/collaboration-workflow/operating-rules.md', bytes: 8192 },
|
|
89
90
|
{ path: 'prompts/collaboration-workflow/output-bounds.md', bytes: 4096 },
|
|
90
91
|
{ path: 'prompts/collaboration-workflow/overview.md', bytes: 4096 },
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { existsSync } from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { find_package_directory } from '#scripts/josh/package-directory'
|
|
5
6
|
import { capped_output } from './capped-print'
|
|
6
7
|
import { document_section } from './document-section'
|
|
7
8
|
import { entry_read_set } from './entry-read-set'
|
|
@@ -35,14 +36,25 @@ const USAGE = 'Usage: josh doc:section <file.md> "<heading>"'
|
|
|
35
36
|
// directory, a bare name that happened to exist beside the caller would beat the document under
|
|
36
37
|
// `root` and print a section of the wrong file — silently, which is the failure this command exists
|
|
37
38
|
// to refuse.
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
//
|
|
40
|
+
// **The kit package is the fallback.** A consumer keeps no `prompts/` of its own — the package ships
|
|
41
|
+
// it under `node_modules/@joshuafolkken/kit/` — so `prompts/review.md` resolves there when `root` has
|
|
42
|
+
// no copy, exactly as `review:brief` resolves the rubric.
|
|
43
|
+
const PACKAGE_DIRECTORY = find_package_directory(path.dirname(fileURLToPath(import.meta.url)))
|
|
40
44
|
|
|
41
|
-
|
|
45
|
+
function candidates_under(directory: string, name: string): Array<string> {
|
|
46
|
+
return [path.resolve(directory, name), entry_read_set.document_path(directory, name)]
|
|
47
|
+
}
|
|
42
48
|
|
|
43
|
-
|
|
49
|
+
function resolve_document(
|
|
50
|
+
name: string,
|
|
51
|
+
root: string = process.cwd(),
|
|
52
|
+
package_directory: string = PACKAGE_DIRECTORY,
|
|
53
|
+
): string {
|
|
54
|
+
const named = path.resolve(root, name)
|
|
55
|
+
const candidates = [...candidates_under(root, name), ...candidates_under(package_directory, name)]
|
|
44
56
|
|
|
45
|
-
return
|
|
57
|
+
return candidates.find((candidate) => existsSync(candidate)) ?? named
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
function report_missing(markdown: string, heading: string): number {
|
|
@@ -47,8 +47,8 @@ const ENTRY_READ_BUDGET: ReadonlyArray<EntryBudget> = [
|
|
|
47
47
|
{ entry: 'kickoff', bytes: 36_864 },
|
|
48
48
|
{ entry: 'fullrun', bytes: 53_248 },
|
|
49
49
|
{ entry: 'halfrun', bytes: 49_152 },
|
|
50
|
-
{ entry: 'prrun', bytes:
|
|
51
|
-
{ entry: 'backlogrun', bytes:
|
|
50
|
+
{ entry: 'prrun', bytes: 53_248 },
|
|
51
|
+
{ entry: 'backlogrun', bytes: 81_920 },
|
|
52
52
|
{ entry: LANE_CHILD, bytes: 45_056 },
|
|
53
53
|
]
|
|
54
54
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { lane_instructions } from '#scripts/agent/lane-instructions'
|
|
3
4
|
import { backlogrun_parent_read_set } from './backlogrun-parent-read-set'
|
|
4
5
|
import { entry_read_set, type ReadSetCost, type SectionCost } from './entry-read-set'
|
|
5
6
|
import { lane_child_read_set } from './lane-child-read-set'
|
|
@@ -192,7 +193,9 @@ function total_lines(report: ReadSetCost, width: number): Array<string> {
|
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
function note_lines(report: ReadSetCost): Array<string> {
|
|
195
|
-
if (report.entry === lane_child_read_set.LANE_CHILD)
|
|
196
|
+
if (report.entry === lane_child_read_set.LANE_CHILD) {
|
|
197
|
+
return [...LANE_CHILD_NOTE, lane_instructions.resident_line(), '']
|
|
198
|
+
}
|
|
196
199
|
|
|
197
200
|
if (report.entry === BACKLOGRUN) return [...BACKLOGRUN_PARENT_NOTE, '']
|
|
198
201
|
|
|
@@ -14,7 +14,6 @@ import { epic_parse } from './epic-parse'
|
|
|
14
14
|
// the children the epic already names, while this one searches for issues the epic does *not* name.
|
|
15
15
|
|
|
16
16
|
const SEARCH_LIMIT = 50
|
|
17
|
-
const PARENT_MARKERS: ReadonlyArray<string> = ['親:', 'Parent:', '親:']
|
|
18
17
|
// Named apart from `epic_audit_checks.ORPHAN_CHILD`: that check reports an orphan it found, this one
|
|
19
18
|
// reports that the search for orphans was incomplete, and a reader has to be able to tell them apart.
|
|
20
19
|
const ORPHAN_SEARCH = 'orphan search'
|
|
@@ -25,18 +24,15 @@ const CAPPED_BY_MATCHES = `The open-issue scan filled its ${String(SEARCH_LIMIT)
|
|
|
25
24
|
|
|
26
25
|
const searched_issue_schema = z.object({ number: z.number(), body: z.string().nullable() })
|
|
27
26
|
|
|
28
|
-
function has_parent_marker(line: string): boolean {
|
|
29
|
-
return PARENT_MARKERS.some((marker) => line.includes(marker))
|
|
30
|
-
}
|
|
31
|
-
|
|
32
27
|
// Whether an issue's body names this epic as its parent, in the shape the issue template uses.
|
|
33
28
|
//
|
|
34
29
|
// Both halves must be on the *same line*. An issue parented to a different epic routinely backlinks
|
|
35
30
|
// this one elsewhere in its body, and matching the marker and the number independently reported
|
|
36
31
|
// every such issue as an orphan.
|
|
37
32
|
//
|
|
38
|
-
// The number is read through the same reference parse the checks use, so a parent line (
|
|
39
|
-
//
|
|
33
|
+
// The number is read through the same reference parse the checks use, so a parent line (one
|
|
34
|
+
// `epic_audit_logic.has_parent_marker` accepts) such as `owner/other#858` names that repository's
|
|
35
|
+
// epic and not this one's.
|
|
40
36
|
// The known set is this repository alone — a parent line names this epic or it names nothing here —
|
|
41
37
|
// and it is passed rather than omitted so a repository whose own name contains a dot still recognizes
|
|
42
38
|
// `joshuafolkken/site.com#858`.
|
|
@@ -49,7 +45,8 @@ function names_this_epic(line: string, epic_number: number, repo: string): boole
|
|
|
49
45
|
function claims_parent(body: string | null, epic_number: number, repo: string): boolean {
|
|
50
46
|
return (body ?? '')
|
|
51
47
|
.split('\n')
|
|
52
|
-
.
|
|
48
|
+
.filter((line) => epic_audit_logic.has_parent_marker(line))
|
|
49
|
+
.some((line) => names_this_epic(line, epic_number, repo))
|
|
53
50
|
}
|
|
54
51
|
|
|
55
52
|
// Why the scan stopped before the end of the open backlog, or that it did not. Both cutoffs hide the
|
|
@@ -180,6 +180,55 @@ function acceptance_section(body: string | undefined): string {
|
|
|
180
180
|
return markdown_section.section_lines_matching(body, is_acceptance_heading).join('\n')
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// Where a body reference that counts as a relation was written. Only these two places state what the
|
|
184
|
+
// issue delivers or where it belongs; a number in the background, symptom or origin section is the
|
|
185
|
+
// example that prompted the filing, and reading it as a relation bundled issues whose work never
|
|
186
|
+
// overlaps.
|
|
187
|
+
type SignalSection = 'acceptance criteria' | 'parent line'
|
|
188
|
+
|
|
189
|
+
interface SignalReference {
|
|
190
|
+
number: number
|
|
191
|
+
section: SignalSection
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// What marks a line as naming the issue's parent, in both spellings for the same reason as
|
|
195
|
+
// `ACCEPTANCE_HEADINGS`. `epic:audit`'s orphan search matches it anywhere in the line, then asks
|
|
196
|
+
// whether the same line names the epic.
|
|
197
|
+
const PARENT_MARKERS: ReadonlyArray<string> = ['親:', '親:', 'Parent:', 'parent:']
|
|
198
|
+
|
|
199
|
+
function has_parent_marker(line: string): boolean {
|
|
200
|
+
return PARENT_MARKERS.some((marker) => line.includes(marker))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// The reference written directly after a parent marker, bold markup allowed (`- **Parent:** #N`).
|
|
204
|
+
// Only that reference is the parent: a background sentence that mentions `parent:` and names an
|
|
205
|
+
// issue elsewhere on the line is not a parent line.
|
|
206
|
+
const PARENT_REFERENCE = /(?:親|Parent|parent)\**[::]\**\s*(?:[\w.-]+\/[\w.-]+)?#\d+/gu
|
|
207
|
+
|
|
208
|
+
function parent_references(body: string): string {
|
|
209
|
+
return markdown_section
|
|
210
|
+
.unfenced_lines(body)
|
|
211
|
+
.flatMap((line) => [...line.matchAll(PARENT_REFERENCE)].map((match) => match[0]))
|
|
212
|
+
.join('\n')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function references_in(text: string, repo: string, section: SignalSection): Array<SignalReference> {
|
|
216
|
+
return parse_references(text, repo).map((number) => ({ number, section }))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// The same-repository references a body makes in the places that count as a relation, each with the
|
|
220
|
+
// section it was read from. A number both places name is reported once, at the first.
|
|
221
|
+
function signal_references(body: string, repo = ''): Array<SignalReference> {
|
|
222
|
+
const read = [
|
|
223
|
+
...references_in(acceptance_section(body), repo, 'acceptance criteria'),
|
|
224
|
+
...references_in(parent_references(body), repo, 'parent line'),
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
return read.filter((reference, index) => {
|
|
228
|
+
return read.findIndex((other) => other.number === reference.number) === index
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
183
232
|
// Everything `node` depends on, directly or through a chain. Walked iteratively so a graph that
|
|
184
233
|
// still contains a cycle terminates — `epic:next` rejects one first, but an auditor must not hang on
|
|
185
234
|
// the input it exists to examine.
|
|
@@ -223,8 +272,10 @@ const epic_audit_logic = {
|
|
|
223
272
|
parse_issue_references,
|
|
224
273
|
parse_references,
|
|
225
274
|
acceptance_section,
|
|
275
|
+
signal_references,
|
|
276
|
+
has_parent_marker,
|
|
226
277
|
depends_on,
|
|
227
278
|
}
|
|
228
279
|
|
|
229
|
-
export type { AuditFinding, FindingLevel, ReferenceState }
|
|
280
|
+
export type { AuditFinding, FindingLevel, ReferenceState, SignalReference, SignalSection }
|
|
230
281
|
export { epic_audit_logic }
|