@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.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/{toolkit-cli → aitk-cli}/REQUIREMENT.md +4 -4
- package/claude/skills/{toolkit-cli → aitk-cli}/SKILL.md +1 -1
- package/claude/skills/{toolkit-feedback → aitk-feedback-file}/REQUIREMENT.md +3 -3
- package/claude/skills/{toolkit-feedback → aitk-feedback-file}/SKILL.md +2 -2
- package/claude/skills/{toolkit-triage → aitk-feedback-triage}/REQUIREMENT.md +3 -3
- package/claude/skills/{toolkit-triage → aitk-feedback-triage}/SKILL.md +3 -3
- package/claude/skills/{toolkit-operator → aitk-operator}/REQUIREMENT.md +3 -3
- package/claude/skills/{toolkit-operator → aitk-operator}/SKILL.md +3 -3
- package/claude/skills/{claude-screencast → aitk-screencast}/REQUIREMENT.md +3 -3
- package/claude/skills/{claude-screencast → aitk-screencast}/SKILL.md +2 -2
- package/claude/skills/{claude-slides-draft → aitk-slides-draft}/REQUIREMENT.md +3 -3
- package/claude/skills/{claude-slides-draft → aitk-slides-draft}/SKILL.md +1 -1
- package/claude/skills/{cli-script → bash-cli-script}/REQUIREMENT.md +2 -2
- package/claude/skills/{cli-script → bash-cli-script}/SKILL.md +2 -2
- package/claude/skills/bash-script/REQUIREMENT.md +2 -2
- package/claude/skills/bash-script/SKILL.md +2 -2
- package/claude/skills/ci-workflow/REQUIREMENT.md +1 -1
- package/claude/skills/claude-memory-review/SKILL.md +2 -2
- package/claude/skills/claude-memory-review/references/receipt-format.md +1 -1
- package/claude/skills/claude-seed-sync/REQUIREMENT.md +1 -1
- package/claude/skills/claude-seed-sync/SKILL.md +1 -1
- package/claude/skills/git-issue/REQUIREMENT.md +2 -2
- package/claude/skills/git-issue/SKILL.md +1 -1
- package/claude/skills/{restate → restate-plainly}/REQUIREMENT.md +2 -2
- package/claude/skills/{restate → restate-plainly}/SKILL.md +2 -2
- package/claude/skills/setup-init/REQUIREMENT.md +1 -1
- package/claude/skills/setup-init/SKILL.md +1 -1
- package/claude/skills/write-human/REQUIREMENT.md +1 -1
- package/claude/skills/write-human/SKILL.md +1 -1
- package/docs/agents/demo.md +1 -1
- package/docs/agents/index.md +1 -0
- package/docs/agents/overview.md +2 -2
- package/docs/agents/scripting.md +1 -1
- package/docs/agents/sessions.md +11 -5
- package/docs/agents/targets.md +83 -0
- package/docs/ai-workflow.md +16 -16
- package/docs/target-projects.md +1 -1
- package/governance/rules/lang/120-bash.md +1 -1
- package/package.json +1 -1
- package/scripts/core/regen-tooling-paths.sh +1 -1
- package/scripts/core/verify.sh +1 -1
- package/src/claude/cases/authoring.ts +2 -2
- package/src/claude/cases/claude-workflow.ts +2 -2
- package/src/claude/cases/setup.ts +6 -6
- package/src/cli.ts +3 -0
- package/src/commands/demo.ts +1 -1
- package/src/commands/sessions.ts +24 -8
- package/src/commands/targets.ts +319 -0
- package/src/demo/beats.ts +1 -1
- package/src/sessions/claim.ts +7 -0
- package/src/sync/stamp.ts +9 -0
- package/src/targets/pulls.ts +250 -0
- package/src/targets/registry.ts +161 -0
- package/src/targets/resolve.ts +145 -0
- package/src/targets/sweep.ts +246 -0
- package/standards/issue.md +1 -1
- /package/claude/skills/{cli-script → bash-cli-script}/references/template.md +0 -0
package/docs/ai-workflow.md
CHANGED
|
@@ -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:
|
|
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:
|
|
270
|
-
| `aitk:
|
|
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
|
|
275
|
-
|
|
|
276
|
-
| `aitk:create-rule`
|
|
277
|
-
| `aitk:create-skill`
|
|
278
|
-
| `aitk:create-snippet`
|
|
279
|
-
| `aitk:create-standard`
|
|
280
|
-
| `aitk:bash-script`
|
|
281
|
-
| `aitk:cli-script`
|
|
282
|
-
| `aitk:ci-workflow`
|
|
283
|
-
| `aitk:
|
|
284
|
-
| `aitk:
|
|
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:
|
|
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`
|
|
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
|
|
package/docs/target-projects.md
CHANGED
|
@@ -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:
|
|
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
|
@@ -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/
|
|
18
|
+
CONTRACT="$PROJECT_ROOT/claude/skills/aitk-cli/SKILL.md"
|
|
19
19
|
|
|
20
20
|
BEGIN="<!-- generated:tooling-paths -->"
|
|
21
21
|
END="<!-- /generated:tooling-paths -->"
|
package/scripts/core/verify.sh
CHANGED
|
@@ -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/
|
|
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: '
|
|
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: '
|
|
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-*`, `
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
|
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: '
|
|
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)
|
package/src/commands/demo.ts
CHANGED
|
@@ -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
|
|
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')
|
package/src/commands/sessions.ts
CHANGED
|
@@ -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
|
|
58
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
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 `
|
|
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
|
*/
|
package/src/sessions/claim.ts
CHANGED
|
@@ -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[]>
|