@erclx/aitk 3.43.2 → 3.44.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.
Files changed (58) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/{toolkit-cli → aitk-cli}/REQUIREMENT.md +4 -4
  3. package/claude/skills/{toolkit-cli → aitk-cli}/SKILL.md +1 -1
  4. package/claude/skills/{toolkit-feedback → aitk-feedback-file}/REQUIREMENT.md +3 -3
  5. package/claude/skills/{toolkit-feedback → aitk-feedback-file}/SKILL.md +2 -2
  6. package/claude/skills/{toolkit-triage → aitk-feedback-triage}/REQUIREMENT.md +3 -3
  7. package/claude/skills/{toolkit-triage → aitk-feedback-triage}/SKILL.md +3 -3
  8. package/claude/skills/{toolkit-operator → aitk-operator}/REQUIREMENT.md +3 -3
  9. package/claude/skills/{toolkit-operator → aitk-operator}/SKILL.md +3 -3
  10. package/claude/skills/{claude-screencast → aitk-screencast}/REQUIREMENT.md +3 -3
  11. package/claude/skills/{claude-screencast → aitk-screencast}/SKILL.md +2 -2
  12. package/claude/skills/{claude-slides-draft → aitk-slides-draft}/REQUIREMENT.md +3 -3
  13. package/claude/skills/{claude-slides-draft → aitk-slides-draft}/SKILL.md +1 -1
  14. package/claude/skills/{cli-script → bash-cli-script}/REQUIREMENT.md +2 -2
  15. package/claude/skills/{cli-script → bash-cli-script}/SKILL.md +2 -2
  16. package/claude/skills/bash-script/REQUIREMENT.md +2 -2
  17. package/claude/skills/bash-script/SKILL.md +2 -2
  18. package/claude/skills/ci-workflow/REQUIREMENT.md +1 -1
  19. package/claude/skills/claude-memory-review/SKILL.md +2 -2
  20. package/claude/skills/claude-memory-review/references/receipt-format.md +1 -1
  21. package/claude/skills/claude-seed-sync/REQUIREMENT.md +1 -1
  22. package/claude/skills/claude-seed-sync/SKILL.md +1 -1
  23. package/claude/skills/git-issue/REQUIREMENT.md +2 -2
  24. package/claude/skills/git-issue/SKILL.md +1 -1
  25. package/claude/skills/{restate → restate-plainly}/REQUIREMENT.md +2 -2
  26. package/claude/skills/{restate → restate-plainly}/SKILL.md +2 -2
  27. package/claude/skills/setup-init/REQUIREMENT.md +1 -1
  28. package/claude/skills/setup-init/SKILL.md +1 -1
  29. package/claude/skills/write-human/REQUIREMENT.md +1 -1
  30. package/claude/skills/write-human/SKILL.md +1 -1
  31. package/docs/agents/demo.md +1 -1
  32. package/docs/agents/index.md +1 -0
  33. package/docs/agents/overview.md +2 -2
  34. package/docs/agents/scripting.md +1 -1
  35. package/docs/agents/sessions.md +11 -5
  36. package/docs/agents/targets.md +83 -0
  37. package/docs/ai-workflow.md +16 -16
  38. package/docs/target-projects.md +1 -1
  39. package/governance/rules/lang/120-bash.md +1 -1
  40. package/package.json +1 -1
  41. package/scripts/core/regen-tooling-paths.sh +1 -1
  42. package/scripts/core/verify.sh +1 -1
  43. package/src/claude/cases/authoring.ts +2 -2
  44. package/src/claude/cases/claude-workflow.ts +2 -2
  45. package/src/claude/cases/setup.ts +6 -6
  46. package/src/cli.ts +3 -0
  47. package/src/commands/demo.ts +1 -1
  48. package/src/commands/sessions.ts +24 -8
  49. package/src/commands/targets.ts +319 -0
  50. package/src/demo/beats.ts +1 -1
  51. package/src/sessions/claim.ts +7 -0
  52. package/src/sync/stamp.ts +9 -0
  53. package/src/targets/pulls.ts +250 -0
  54. package/src/targets/registry.ts +161 -0
  55. package/src/targets/resolve.ts +145 -0
  56. package/src/targets/sweep.ts +246 -0
  57. package/standards/issue.md +1 -1
  58. /package/claude/skills/{cli-script → bash-cli-script}/references/template.md +0 -0
@@ -185,7 +185,7 @@ This section is the corpus the coverage claim is measured against: every name `a
185
185
  | Skill | When to use |
186
186
  | ---------------------------- | --------------------------------------------------------------------------------------- |
187
187
  | `aitk:setup-init` | On a fresh scaffold, to detect the stack and run the whole install chain in one pass |
188
- | `aitk:toolkit-operator` | On a project that already exists, to read what it carries before an install is picked |
188
+ | `aitk:aitk-operator` | On a project that already exists, to read what it carries before an install is picked |
189
189
  | `aitk:setup-gov` | When the governance rules are wanted without the tooling chain |
190
190
  | `aitk:setup-indexes` | When a markdown-heavy folder needs an `index.md` a session can browse |
191
191
  | `aitk:setup-plugins` | On a new machine, to install the community and official plugins user-scoped |
@@ -266,32 +266,32 @@ This section is the corpus the coverage claim is measured against: every name `a
266
266
  | `aitk:migration-context` | When `docs/` holds agent-flavored files belonging in `.claude/context/` |
267
267
  | `aitk:migration-superseded` | When a drift report names a `.claude/` file a folder has replaced |
268
268
  | `aitk:migration-standards-drop` | When the project still holds an installed `.claude/standards/` tree |
269
- | `aitk:toolkit-feedback` | When something in the toolkit is broken, missing, or off |
270
- | `aitk:toolkit-triage` | In the toolkit repo, to work through the open feedback issues |
269
+ | `aitk:aitk-feedback-file` | When something in the toolkit is broken, missing, or off |
270
+ | `aitk:aitk-feedback-triage` | In the toolkit repo, to work through the open feedback issues |
271
271
 
272
272
  ### Generate an artifact on demand
273
273
 
274
- | Skill | When to use |
275
- | -------------------------- | --------------------------------------------------------------------- |
276
- | `aitk:create-rule` | For a project-specific governance rule the toolkit does not ship |
277
- | `aitk:create-skill` | For a new `SKILL.md` |
278
- | `aitk:create-snippet` | For a reusable prompt |
279
- | `aitk:create-standard` | For a new authoring convention |
280
- | `aitk:bash-script` | For an interactive, human-facing shell tool |
281
- | `aitk:cli-script` | For a non-interactive automation, CI, or pipeline script |
282
- | `aitk:ci-workflow` | For a GitHub Actions workflow file |
283
- | `aitk:claude-slides-draft` | For a deck, drafted as `.claude/SLIDES.md` and rendered to PowerPoint |
284
- | `aitk:claude-screencast` | For a recording script with beats and defaults already seeded |
274
+ | Skill | When to use |
275
+ | ------------------------ | --------------------------------------------------------------------- |
276
+ | `aitk:create-rule` | For a project-specific governance rule the toolkit does not ship |
277
+ | `aitk:create-skill` | For a new `SKILL.md` |
278
+ | `aitk:create-snippet` | For a reusable prompt |
279
+ | `aitk:create-standard` | For a new authoring convention |
280
+ | `aitk:bash-script` | For an interactive, human-facing shell tool |
281
+ | `aitk:bash-cli-script` | For a non-interactive automation, CI, or pipeline script |
282
+ | `aitk:ci-workflow` | For a GitHub Actions workflow file |
283
+ | `aitk:aitk-slides-draft` | For a deck, drafted as `.claude/SLIDES.md` and rendered to PowerPoint |
284
+ | `aitk:aitk-screencast` | For a recording script with beats and defaults already seeded |
285
285
 
286
286
  ### Answer a question at any point
287
287
 
288
288
  | Skill | When to use |
289
289
  | -------------------------- | ------------------------------------------------------------------------------ |
290
- | `aitk:toolkit-cli` | Before a sync or install, to learn what it overwrites, merges, or leaves alone |
290
+ | `aitk:aitk-cli` | Before a sync or install, to learn what it overwrites, merges, or leaves alone |
291
291
  | `aitk:youtube-transcripts` | When a video transcript is wanted in the repo as context |
292
292
  | `aitk:claude-teach` | To learn a subject across sessions, in a workspace that holds the progress |
293
293
  | `aitk:write-human` | Before drafting or revising prose, for voice, rhythm, and density |
294
- | `aitk:restate` | When an answer or a document has to be read again in plain words |
294
+ | `aitk:restate-plainly` | When an answer or a document has to be read again in plain words |
295
295
 
296
296
  Every row answers a question rather than marking a point in a project's life, so a phase above would send a reader to the wrong group.
297
297
 
@@ -49,7 +49,7 @@ The chain is:
49
49
  - The agent reads `aitk tooling reference <stack>` (plus parents) as its audit context, follows it to generate eslint, vitest, playwright configs and the stack's setup script, and extends `.claude/context/ci.md` and `.claude/context/development.md` per the reference's extend sections <!-- audit-ignore-citations: .claude/context/development.md -->
50
50
  - `setup-verify` runs the installed `package.json` scripts (lint, typecheck, check, test, build) and reports pass or fail
51
51
 
52
- The chain serves a fresh scaffold and names a destination for the three states it does not. An existing project goes to `aitk:toolkit-operator`, which reads what the project already carries before naming a per-domain command. An install wanting the Claude layer without the tooling chain runs `aitk claude init` for the seed docs and then `aitk:setup-indexes` for the index system. A language the toolkit ships no stack for is the one of the three the chain still runs for, on `base`, with the fallback marked in the preview so it can be declined there.
52
+ The chain serves a fresh scaffold and names a destination for the three states it does not. An existing project goes to `aitk:aitk-operator`, which reads what the project already carries before naming a per-domain command. An install wanting the Claude layer without the tooling chain runs `aitk claude init` for the seed docs and then `aitk:setup-indexes` for the index system. A language the toolkit ships no stack for is the one of the three the chain still runs for, on `base`, with the fallback marked in the preview so it can be declined there.
53
53
 
54
54
  Keep the `## Scripts` table in `.claude/context/development.md` current as scripts are added. Base tooling seeds that entry with the commands it installs, and each stack reference extends the table. `project-commands` reads it to start the app or run a check on request, so a command missing from the table cannot be run that way. A project whose entry outgrew one file and split into `.claude/context/development/` keeps the table in `overview.md`, which is where the skill looks next. <!-- audit-ignore-citations: .claude/context/development.md -->
55
55
 
@@ -9,7 +9,7 @@ paths:
9
9
  ## Skill routing
10
10
 
11
11
  - Use `aitk:bash-script` for an interactive or human-facing script: prompts, a visual timeline UI, framed terminal output.
12
- - Use `aitk:cli-script` for a non-interactive script: automation, CI, cron, a pipeline helper, or anything run by an agent rather than watched by a person.
12
+ - Use `aitk:bash-cli-script` for a non-interactive script: automation, CI, cron, a pipeline helper, or anything run by an agent rather than watched by a person.
13
13
  - Load the matched skill's own reference templates rather than hand-rolling interactivity or logging patterns outside them.
14
14
  - Report it rather than proceeding silently when the matched skill does not resolve. Both ship with the plugin and this rule ships with the CLI, so a project that installed governance alone does not have them.
15
15
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "3.43.2",
4
+ "version": "3.44.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -15,7 +15,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
15
  PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
16
16
 
17
17
  TOOLING_DIR="$PROJECT_ROOT/tooling"
18
- CONTRACT="$PROJECT_ROOT/claude/skills/toolkit-cli/SKILL.md"
18
+ CONTRACT="$PROJECT_ROOT/claude/skills/aitk-cli/SKILL.md"
19
19
 
20
20
  BEGIN="<!-- generated:tooling-paths -->"
21
21
  END="<!-- /generated:tooling-paths -->"
@@ -298,7 +298,7 @@ main() {
298
298
 
299
299
  log_step "Tooling paths"
300
300
  run_check "bash $PROJECT_ROOT/scripts/core/regen-tooling-paths.sh" "Tooling-path regen failed"
301
- assert_no_drift "claude/skills/toolkit-cli/SKILL.md" "The overwrite contract drifted from what the stacks hold. Run bun run check and commit claude/skills/toolkit-cli/SKILL.md."
301
+ assert_no_drift "claude/skills/aitk-cli/SKILL.md" "The overwrite contract drifted from what the stacks hold. Run bun run check and commit claude/skills/aitk-cli/SKILL.md."
302
302
  log_info "Tooling paths clean"
303
303
 
304
304
  # The claude manifest is the only route a target's ignore set travels, and it
@@ -18,7 +18,7 @@ export const AUTHORING_CASES: readonly SkillCase[] = [
18
18
  {
19
19
  prompt:
20
20
  'Write me a headless automation script with no interactive prompts.',
21
- expect: 'cli-script',
21
+ expect: 'bash-cli-script',
22
22
  },
23
23
  {
24
24
  prompt: 'Scaffold a brand-new SKILL.md for this capability.',
@@ -39,7 +39,7 @@ export const AUTHORING_CASES: readonly SkillCase[] = [
39
39
  },
40
40
  {
41
41
  prompt: 'Say what that dense answer actually means in plain terms.',
42
- expect: 'restate',
42
+ expect: 'restate-plainly',
43
43
  },
44
44
  {
45
45
  prompt: 'This passage reads flat and robotic, give it some real cadence.',
@@ -81,7 +81,7 @@ export const CLAUDE_WORKFLOW_CASES: readonly SkillCase[] = [
81
81
  },
82
82
  {
83
83
  prompt: 'Draft me a script with beats for a screencast recording.',
84
- expect: 'claude-screencast',
84
+ expect: 'aitk-screencast',
85
85
  },
86
86
  {
87
87
  prompt:
@@ -90,7 +90,7 @@ export const CLAUDE_WORKFLOW_CASES: readonly SkillCase[] = [
90
90
  },
91
91
  {
92
92
  prompt: 'Turn this topic into a slide deck I can render.',
93
- expect: 'claude-slides-draft',
93
+ expect: 'aitk-slides-draft',
94
94
  },
95
95
  {
96
96
  prompt:
@@ -1,7 +1,7 @@
1
1
  import type { SkillCase } from '@/claude/skills-rank'
2
2
 
3
3
  /**
4
- * `setup-*`, `migration-*`, `toolkit-*`, and `create-rule`: scaffolding,
4
+ * `setup-*`, `migration-*`, `aitk-*`, and `create-rule`: scaffolding,
5
5
  * proposal-only migrations, and the toolkit's own reference and feedback
6
6
  * surfaces.
7
7
  */
@@ -53,20 +53,20 @@ export const SETUP_CASES: readonly SkillCase[] = [
53
53
  {
54
54
  prompt:
55
55
  "Before I run this sync, tell me exactly what it's going to overwrite.",
56
- expect: 'toolkit-cli',
56
+ expect: 'aitk-cli',
57
57
  },
58
58
  {
59
59
  prompt:
60
60
  'Something about the toolkit itself is broken, write it up and send it back to the maintainers.',
61
- expect: 'toolkit-feedback',
61
+ expect: 'aitk-feedback-file',
62
62
  },
63
63
  {
64
64
  prompt:
65
65
  "I don't know which specific toolkit skill I need, just handle it for me.",
66
- expect: 'toolkit-operator',
66
+ expect: 'aitk-operator',
67
67
  },
68
68
  // The performing skill wins a phrase naming its operation over the front
69
- // door's own routing framing, even though toolkit-operator's description
69
+ // door's own routing framing, even though aitk-operator's description
70
70
  // quotes a phrase near this one. See .claude/context/cli/audits.md.
71
71
  {
72
72
  prompt:
@@ -76,7 +76,7 @@ export const SETUP_CASES: readonly SkillCase[] = [
76
76
  {
77
77
  prompt:
78
78
  'Work through the open feedback issues on the toolkit repo one by one.',
79
- expect: 'toolkit-triage',
79
+ expect: 'aitk-feedback-triage',
80
80
  },
81
81
  {
82
82
  prompt:
package/src/cli.ts CHANGED
@@ -33,6 +33,7 @@ import { register as secrets } from '@/commands/secrets'
33
33
  import { register as deps } from '@/commands/deps'
34
34
  import { register as labels } from '@/commands/labels'
35
35
  import { register as census } from '@/commands/census'
36
+ import { register as targets } from '@/commands/targets'
36
37
  import { register as upgrade } from '@/commands/upgrade'
37
38
  import { readInstalled, UNKNOWN_LABEL } from '@/version/installed'
38
39
  import { palette } from '@/ui'
@@ -72,6 +73,7 @@ function showHelp(): void {
72
73
  `${GREY}│${NC} markdown [cmd] ${GREY}# Report markdown against the attribute standards (audit)${NC}`,
73
74
  `${GREY}│${NC} records [cmd] ${GREY}# Session records under .claude/ (validate, size, push, pull)${NC}`,
74
75
  `${GREY}│${NC} sessions [cmd] ${GREY}# Resolve live sessions to worktree and branch (list)${NC}`,
76
+ `${GREY}│${NC} targets [cmd] ${GREY}# Report the projects this toolkit installed into (list, pulls)${NC}`,
75
77
  `${GREY}│${NC} worktrees [cmd] ${GREY}# Report which worktrees are reclaimable (list)${NC}`,
76
78
  `${GREY}│${NC} secrets [cmd] ${GREY}# Read the shipped tree for credential-shaped values (scan)${NC}`,
77
79
  `${GREY}│${NC} deps [cmd] ${GREY}# Read the resolved dependency set for advisories (audit)${NC}`,
@@ -170,6 +172,7 @@ context(program)
170
172
  markdown(program)
171
173
  records(program)
172
174
  sessions(program)
175
+ targets(program)
173
176
  worktrees(program)
174
177
  secrets(program)
175
178
  deps(program)
@@ -42,7 +42,7 @@ export function register(program: Command): void {
42
42
  demo
43
43
  .command('compile')
44
44
  .description('Turn a screencast draft into a plan a run can drive')
45
- .argument('<draft>', 'Screencast draft written by claude-screencast')
45
+ .argument('<draft>', 'Screencast draft written by aitk-screencast')
46
46
  .helpOption('-h, --help', 'Show this help message')
47
47
  .option('-o, --out <dir>', 'Directory the plan is written to', DEFAULT_OUT)
48
48
  .option('-s, --slug <slug>', 'Plan name, defaulting to the draft filename')
@@ -1,3 +1,4 @@
1
+ import { resolve } from 'node:path'
1
2
  import type { Command } from 'commander'
2
3
  import { checkClaim, type ClaimReport } from '@/sessions/claim'
3
4
  import {
@@ -19,6 +20,7 @@ import {
19
20
  interface ListCommandOptions {
20
21
  readonly json?: boolean
21
22
  readonly branch?: string
23
+ readonly repository?: string
22
24
  }
23
25
 
24
26
  const REASONS: Record<string, string> = {
@@ -46,6 +48,10 @@ export function register(program: Command): void {
46
48
  '--branch <name>',
47
49
  'Report only the sessions holding this branch in this repository',
48
50
  )
51
+ .option(
52
+ '--repository <path>',
53
+ 'Answer about this project rather than the working one',
54
+ )
49
55
  .addHelpText(
50
56
  'after',
51
57
  [
@@ -54,11 +60,18 @@ export function register(program: Command): void {
54
60
  ' 0 the roster was read',
55
61
  ' 1 refused, with the reason on stderr',
56
62
  '',
57
- '--branch scopes the match to the repository the command runs in, since',
58
- 'a branch name identifies a branch there and nothing across a machine.',
63
+ '--branch scopes the match to one repository, since a branch name',
64
+ 'identifies a branch there and nothing across a machine. That is the',
65
+ 'repository the command runs in unless --repository names another.',
59
66
  'A bare run reports every repository and carries the repository field,',
60
67
  'so a caller filtering by hand has something that identifies one.',
61
68
  '',
69
+ '--repository moves every reading to the project at that path, the',
70
+ 'session match and the worktree and ref reads alike. The roster is',
71
+ 'machine-wide already, so this is what lets a dispatcher in one project',
72
+ 'see a branch a live session holds in another rather than reading it as',
73
+ 'unclaimed and sending a second session onto it.',
74
+ '',
62
75
  'With --branch, the JSON also carries "worktree" (the path of any',
63
76
  'worktree already checked out to it, or null), "refs" (the refs that',
64
77
  'already name it, local head and origin remote-tracking alike), and',
@@ -94,6 +107,7 @@ export function register(program: Command): void {
94
107
  ' aitk sessions list',
95
108
  ' aitk sessions list --json',
96
109
  ' aitk sessions list --branch feat/parser --json',
110
+ ' aitk sessions list --branch chore/agents --repository ../caret --json',
97
111
  '',
98
112
  ].join('\n'),
99
113
  )
@@ -125,13 +139,18 @@ async function runList(opts: ListCommandOptions): Promise<number> {
125
139
  // A branch name identifies a branch inside one repository and nothing across
126
140
  // a machine, so an unscoped match reaches a session working in a different
127
141
  // project. `main` is the name that collides on every machine running two.
128
- const repository = opts.branch ? await repositoryOf(process.cwd()) : null
142
+ //
143
+ // Which repository that is is the caller's to name. The roster this filters
144
+ // is machine-wide already, so a dispatcher asking about another project was
145
+ // answered "unclaimed" about a branch a live session there was holding.
146
+ const at = opts.repository ? resolve(opts.repository) : process.cwd()
147
+ const repository = opts.branch ? await repositoryOf(at) : null
129
148
 
130
149
  if (opts.branch && repository === null) {
131
150
  intro('aitk sessions list')
132
151
  logStep('Refused')
133
152
  logWarn(
134
- '--branch scopes the match to the repository this command runs in, and no repository resolved here. Run it inside one, or read the whole roster and filter on the repository field.',
153
+ `--branch scopes the match to one repository, and none resolved at ${at}. Run it inside one, name another with --repository, or read the whole roster and filter on the repository field.`,
135
154
  )
136
155
  outro()
137
156
 
@@ -152,10 +171,7 @@ async function runList(opts: ListCommandOptions): Promise<number> {
152
171
  : report.sessions
153
172
 
154
173
  const claim = opts.branch
155
- ? await checkClaim(opts.branch, {
156
- cwd: process.cwd(),
157
- resolve: async () => report,
158
- })
174
+ ? await checkClaim(opts.branch, { cwd: at, resolve: async () => report })
159
175
  : null
160
176
 
161
177
  intro('aitk sessions list')
@@ -0,0 +1,319 @@
1
+ import type { Command } from 'commander'
2
+ import { readPullsAcross, type TargetPulls } from '@/targets/pulls'
3
+ import {
4
+ type KnownTarget,
5
+ type ResolvedTargets,
6
+ resolveTargets,
7
+ } from '@/targets/resolve'
8
+ import { DEFAULT_DEPTH, type SweepBound } from '@/targets/sweep'
9
+ import {
10
+ intro,
11
+ logInfo,
12
+ logStep,
13
+ logWarn,
14
+ outro,
15
+ pipeOutput,
16
+ plural,
17
+ } from '@/ui'
18
+
19
+ interface ListOptions {
20
+ readonly json?: boolean
21
+ readonly sweep?: string[]
22
+ readonly depth?: string
23
+ }
24
+
25
+ interface PullsOptions extends ListOptions {}
26
+
27
+ /**
28
+ * Reads the depth a sweep is bounded by, or null when the value is not one.
29
+ *
30
+ * `Number('abc')` is `NaN` and every `level >= NaN` test is false, so an
31
+ * unchecked value walks to the bottom of whatever root it was given while
32
+ * the bound reports a depth of `NaN`. Both halves of the guarantee go at once,
33
+ * which is why this refuses rather than falling back to the default.
34
+ */
35
+ function readDepth(value: string | undefined): number | null {
36
+ const depth = Number(value ?? DEFAULT_DEPTH)
37
+
38
+ return Number.isInteger(depth) && depth >= 0 ? depth : null
39
+ }
40
+
41
+ const REFUSALS: Record<string, string> = {
42
+ 'not-a-directory': 'the path is not a directory, so nothing was read there',
43
+ 'gh-unavailable': 'gh is not on the path, so no pull request could be read',
44
+ 'list-failed':
45
+ 'the open pull request list could not be read, so this is not a target with no work',
46
+ }
47
+
48
+ export function register(program: Command): void {
49
+ const targets = program
50
+ .command('targets')
51
+ .description('Report the projects this toolkit has installed into')
52
+ .helpOption('-h, --help', 'Show this help message')
53
+
54
+ targets
55
+ .command('list')
56
+ .description('Report every known target with where the answer came from')
57
+ .helpOption('-h, --help', 'Show this help message')
58
+ .option('--json', 'Add a machine-readable record on stdout')
59
+ .option(
60
+ '--sweep <path...>',
61
+ 'Also walk these roots for targets the record never held',
62
+ )
63
+ .option(
64
+ '--depth <n>',
65
+ 'How deep below each swept root to walk',
66
+ String(DEFAULT_DEPTH),
67
+ )
68
+ .addHelpText('after', LIST_HELP)
69
+ .action(async (opts: ListOptions) => {
70
+ process.exitCode = await runList(opts)
71
+ })
72
+
73
+ targets
74
+ .command('pulls')
75
+ .description(
76
+ 'Report the open pull request, checks, and review heading per target',
77
+ )
78
+ .argument('[path...]', 'Targets to read, defaulting to every known one')
79
+ .helpOption('-h, --help', 'Show this help message')
80
+ .option('--json', 'Add a machine-readable record on stdout')
81
+ .option('--sweep <path...>', 'Also walk these roots when no path is given')
82
+ .option(
83
+ '--depth <n>',
84
+ 'How deep below each swept root to walk',
85
+ String(DEFAULT_DEPTH),
86
+ )
87
+ .addHelpText('after', PULLS_HELP)
88
+ .action(async (paths: string[], opts: PullsOptions) => {
89
+ process.exitCode = await runPulls(paths, opts)
90
+ })
91
+ }
92
+
93
+ const LIST_HELP = [
94
+ '',
95
+ 'Sources:',
96
+ ' Every sync that stamps a target records it in a machine-level index, so',
97
+ ' a project installed since that shipped is known without being named.',
98
+ ' --sweep walks the roots given for anything installed before it, and a',
99
+ ' target found in two clones is reported once with both paths.',
100
+ '',
101
+ 'Exit codes:',
102
+ ' 0 the population was read',
103
+ ' 1 refused, with the reason on stderr',
104
+ '',
105
+ 'An absent index is reported as unknown rather than as no targets. Nothing',
106
+ 'was read in that case, so a count of zero would be a confident wrong answer.',
107
+ '',
108
+ 'The JSON carries a "bound" object whenever a sweep ran, naming the roots',
109
+ 'walked, the depth, the folders the walk stopped at, the roots it could not',
110
+ 'read, and the directory symlinks the walk did not follow. A sweep cannot',
111
+ 'see another machine or a clone under a path nobody named, so read the bound',
112
+ 'before treating the count as the population.',
113
+ '',
114
+ 'An exit code says nothing about a call made from a session, since a shell',
115
+ 'profile may wrap the binary in a function taking its status from a later',
116
+ 'command. Read the record rather than the exit when a skill consumes this.',
117
+ '',
118
+ 'Examples:',
119
+ ' aitk targets list',
120
+ ' aitk targets list --json',
121
+ ' aitk targets list --sweep ~/repos --json',
122
+ '',
123
+ ].join('\n')
124
+
125
+ const PULLS_HELP = [
126
+ '',
127
+ 'Reads, per target, every open pull request with its checks and the heading',
128
+ 'its newest review pass carries. Naming paths reads those and looks up',
129
+ 'nothing; naming none reads every target `aitk targets list` reports.',
130
+ '',
131
+ 'Exit codes:',
132
+ ' 0 at least one target was read',
133
+ ' 1 refused, or every target refused',
134
+ '',
135
+ 'A target that could not be read carries a "reason" rather than an empty',
136
+ 'pull list, since reading a failed query as no open work is what reports a',
137
+ 'target as done having read nothing.',
138
+ '',
139
+ '"checks" is null when GitHub reported no check at all, which is not the',
140
+ 'same answer as passing. "review" is "open" while the newest pass carries',
141
+ '## Review, "closed" once one carries ## Review closed, and null when no',
142
+ 'pass has landed. "reviewReadable" is false when that query failed, which',
143
+ 'leaves "review" covering nothing.',
144
+ '',
145
+ 'Examples:',
146
+ ' aitk targets pulls',
147
+ ' aitk targets pulls ../caret ../stackr --json',
148
+ '',
149
+ ].join('\n')
150
+
151
+ async function runList(opts: ListOptions): Promise<number> {
152
+ intro('aitk targets list')
153
+
154
+ const depth = readDepth(opts.depth)
155
+
156
+ if (depth === null) return refuseDepth(opts)
157
+
158
+ const resolved = await resolveTargets({ sweep: opts.sweep, depth })
159
+
160
+ const unknown = reportTargets(resolved)
161
+ if (resolved.bound) reportBound(resolved.bound)
162
+ outro()
163
+
164
+ if (opts.json) {
165
+ process.stdout.write(
166
+ `${JSON.stringify({
167
+ registry: resolved.registry?.path ?? null,
168
+ known: resolved.registry?.kind !== 'absent',
169
+ targets: resolved.targets,
170
+ bound: resolved.bound,
171
+ })}\n`,
172
+ )
173
+ }
174
+
175
+ return unknown ? 1 : 0
176
+ }
177
+
178
+ async function runPulls(paths: string[], opts: PullsOptions): Promise<number> {
179
+ intro('aitk targets pulls')
180
+
181
+ const depth = readDepth(opts.depth)
182
+
183
+ if (depth === null) return refuseDepth(opts)
184
+
185
+ const resolved = await resolveTargets({ paths, sweep: opts.sweep, depth })
186
+
187
+ if (resolved.targets.length === 0) {
188
+ logStep('Refused')
189
+ logWarn(
190
+ resolved.registry?.kind === 'absent'
191
+ ? `No target index at ${resolved.registry.path}, and no path was given. Name the targets, or sweep for them with --sweep.`
192
+ : 'No target resolved, so nothing was read.',
193
+ )
194
+ outro()
195
+
196
+ if (opts.json) {
197
+ process.stdout.write(
198
+ `${JSON.stringify({ reason: 'no-targets', targets: [] })}\n`,
199
+ )
200
+ }
201
+
202
+ return 1
203
+ }
204
+
205
+ // One clone per project. Two checkouts sharing an origin answer the same
206
+ // query, so reading both spends the rate limit to print one answer twice.
207
+ const reports = await readPullsAcross(
208
+ resolved.targets.map((target) => target.paths[0] ?? ''),
209
+ )
210
+
211
+ reportPulls(reports)
212
+ outro()
213
+
214
+ if (opts.json) {
215
+ process.stdout.write(`${JSON.stringify({ targets: reports })}\n`)
216
+ }
217
+
218
+ return reports.every((report) => report.kind === 'refused') ? 1 : 0
219
+ }
220
+
221
+ function refuseDepth(opts: ListOptions): number {
222
+ logStep('Refused')
223
+ logWarn(
224
+ `--depth takes a whole number of levels and was given ${opts.depth}. A value that is not one leaves the walk with no cap at all.`,
225
+ )
226
+ outro()
227
+
228
+ if (opts.json) {
229
+ process.stdout.write(
230
+ `${JSON.stringify({ reason: 'bad-depth', depth: opts.depth, targets: [] })}\n`,
231
+ )
232
+ }
233
+
234
+ return 1
235
+ }
236
+
237
+ /** Returns whether the population is unknown, which is the one refusal this read has. */
238
+ function reportTargets(resolved: ResolvedTargets): boolean {
239
+ logStep('Targets')
240
+
241
+ if (resolved.registry?.kind === 'absent' && resolved.bound === null) {
242
+ logWarn(
243
+ `No target index at ${resolved.registry.path}. Nothing was read, so this is not a machine with no targets. Sweep for them with --sweep, or run a sync in a target to record it.`,
244
+ )
245
+ return true
246
+ }
247
+
248
+ if (resolved.targets.length === 0) {
249
+ logInfo('No target found.')
250
+ return false
251
+ }
252
+
253
+ logInfo(plural(resolved.targets.length, 'target'))
254
+ pipeOutput(resolved.targets.map(describe).join('\n'))
255
+ return false
256
+ }
257
+
258
+ function describe(target: KnownTarget): string {
259
+ const flags = [target.source, ...(target.legacy ? ['legacy stamp'] : [])]
260
+ const clones =
261
+ target.paths.length > 1 ? `\n ${target.paths.slice(1).join('\n ')}` : ''
262
+
263
+ return `${target.paths[0]} ${flags.join(', ')}${clones}`
264
+ }
265
+
266
+ function reportBound(bound: SweepBound): void {
267
+ logStep('Bound')
268
+ logInfo(
269
+ `Walked ${plural(bound.roots.length, 'root')} to depth ${bound.depth} on this machine alone.`,
270
+ )
271
+
272
+ if (bound.truncated.length > 0) {
273
+ logWarn(
274
+ `${plural(bound.truncated.length, 'folder')} hit the depth cap, so a target below one is unseen.`,
275
+ )
276
+ }
277
+
278
+ if (bound.unreadable.length > 0) {
279
+ logWarn(
280
+ `${plural(bound.unreadable.length, 'root')} could not be read: ${bound.unreadable.join(', ')}`,
281
+ )
282
+ }
283
+
284
+ if (bound.symlinks.length > 0) {
285
+ const verb = bound.symlinks.length === 1 ? 'was' : 'were'
286
+ logWarn(
287
+ `${plural(bound.symlinks.length, 'symlink')} to a directory ${verb} not followed: ${bound.symlinks.join(', ')}`,
288
+ )
289
+ }
290
+ }
291
+
292
+ function reportPulls(reports: readonly TargetPulls[]): void {
293
+ logStep('Pull requests')
294
+
295
+ for (const report of reports) {
296
+ if (report.kind === 'refused') {
297
+ logWarn(`${report.path}: ${REFUSALS[report.reason] ?? report.reason}`)
298
+ continue
299
+ }
300
+
301
+ if (report.pulls.length === 0) {
302
+ logInfo(`${report.path}: nothing open.`)
303
+ continue
304
+ }
305
+
306
+ logInfo(`${report.path}`)
307
+ pipeOutput(
308
+ report.pulls
309
+ .map((pull) => {
310
+ const checks = pull.checks ?? 'no checks'
311
+ const review = pull.reviewReadable
312
+ ? (pull.review ?? 'no pass')
313
+ : 'review unreadable'
314
+ return ` #${pull.number} ${checks} ${review} ${pull.url}`
315
+ })
316
+ .join('\n'),
317
+ )
318
+ }
319
+ }
package/src/demo/beats.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Reads the human-facing draft `claude-screencast` writes. Nothing here knows
2
+ * Reads the human-facing draft `aitk-screencast` writes. Nothing here knows
3
3
  * about a browser: the draft is prose aimed at a person, and turning it into
4
4
  * something executable is `@/demo/compile`'s job.
5
5
  */
@@ -24,6 +24,13 @@ export interface ClaimReport {
24
24
  }
25
25
 
26
26
  export interface ClaimOptions {
27
+ /**
28
+ * The repository the claim is answered about, not merely where the caller
29
+ * stands. Every reading below is taken against it, so handing another
30
+ * project's path asks about that project, which is what lets a dispatcher in
31
+ * one repository see a branch held in another. Defaults to the caller's own
32
+ * directory, so a call omitting it answers exactly as it always has.
33
+ */
27
34
  readonly cwd?: string
28
35
  readonly resolve?: () => Promise<SessionReport>
29
36
  readonly listWorktrees?: (cwd: string) => Promise<readonly WorktreeEntry[]>