@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
@@ -0,0 +1,246 @@
1
+ import { readdirSync } from 'node:fs'
2
+ import { join, resolve } from 'node:path'
3
+ import { $ } from 'bun'
4
+ import { gitEnv } from '@/git-env'
5
+ import { isLegacyStamped, legacyStampPath, stampPath } from '@/sync/stamp'
6
+ import { isDirectory } from '@/target'
7
+
8
+ /**
9
+ * Folders a walk never descends into.
10
+ *
11
+ * Most hold vendored trees or the repository's own object store, where a target
12
+ * cannot live and walking costs the sweep most of its time. `.claude` is here
13
+ * for a different reason: a stamped folder is found by testing the folder
14
+ * itself rather than by walking into its `.claude`, so descending finds nothing
15
+ * new, and `.claude/worktrees/` holds a full checkout per linked worktree that
16
+ * carries a copy of its own target's stamp. One target on this machine had five
17
+ * of them, each of which would have reported as a target of its own.
18
+ */
19
+ const SKIP = new Set([
20
+ 'node_modules',
21
+ '.git',
22
+ '.claude',
23
+ 'dist',
24
+ 'build',
25
+ 'vendor',
26
+ 'target',
27
+ '.next',
28
+ '.venv',
29
+ ])
30
+
31
+ /** How deep below a root the walk goes before it stops and says so. */
32
+ export const DEFAULT_DEPTH = 4
33
+
34
+ /**
35
+ * One project, with every path on this machine that holds a stamp for it.
36
+ *
37
+ * The paths are plural because a project can be cloned more than once, which
38
+ * is not a hypothetical: the census taken on 2026-08-28 counted `caret` at the
39
+ * clone it walked while the repair had run in a second clone it never saw, and
40
+ * that gap is what left a task carrying a ticked outcome its own finding
41
+ * contradicted.
42
+ */
43
+ export interface SweptTarget {
44
+ readonly paths: readonly string[]
45
+ /** The origin every path agrees on, or null when git resolved none. */
46
+ readonly origin: string | null
47
+ /** True while every path still carries its stamp at the retired location. */
48
+ readonly legacy: boolean
49
+ }
50
+
51
+ /**
52
+ * What the answer is bounded by, reported alongside it.
53
+ *
54
+ * A sweep cannot see another machine, a clone under a path nobody named, or a
55
+ * tree it lacked permission to read. Stating the bound is what makes an
56
+ * incomplete answer legible as incomplete, which the two undercounts this
57
+ * exists to replace were not.
58
+ */
59
+ export interface SweepBound {
60
+ readonly roots: readonly string[]
61
+ readonly depth: number
62
+ /** Folders the walk stopped at on reaching the depth cap, so a target below one is unseen. */
63
+ readonly truncated: readonly string[]
64
+ /** Roots that could not be listed at all, as opposed to holding nothing. */
65
+ readonly unreadable: readonly string[]
66
+ /**
67
+ * Symlinks to directories, which the walk does not follow.
68
+ *
69
+ * `readdirSync` with `withFileTypes` answers `isDirectory()` false for one, so
70
+ * without this field a target reached only through a symlink is dropped
71
+ * before the walk and named nowhere, which is a third silent undercount
72
+ * beside the two this module replaces.
73
+ */
74
+ readonly symlinks: readonly string[]
75
+ }
76
+
77
+ export interface SweepReport {
78
+ readonly targets: readonly SweptTarget[]
79
+ readonly bound: SweepBound
80
+ }
81
+
82
+ export interface SweepOptions {
83
+ readonly depth?: number
84
+ /** Resolves a checkout's origin. Injected so a test needs no remote. */
85
+ readonly originOf?: (path: string) => Promise<string | null>
86
+ }
87
+
88
+ /** Whether a folder carries an install stamp at either the current or the retired path. */
89
+ function isStamped(path: string): boolean {
90
+ return (
91
+ Bun.file(stampPath(path)).size > 0 ||
92
+ Bun.file(legacyStampPath(path)).size > 0
93
+ )
94
+ }
95
+
96
+ /**
97
+ * Reads the origin a checkout pushes to, trimmed to a form two clones of one
98
+ * project agree on.
99
+ *
100
+ * The scheme and the `.git` suffix are dropped because one project is commonly
101
+ * cloned over ssh in one place and https in another, and a comparison keeping
102
+ * either reports those as two projects, which is the count this exists to fix.
103
+ *
104
+ * Userinfo goes with them, and it is the component that has to. A checkout
105
+ * cloned as `https://x-access-token:<token>@github.com/owner/repo.git` puts the
106
+ * token in the key, and the key is returned on `SweptTarget.origin` and printed
107
+ * by `aitk targets list --json`. Dropping it also fixes the count, since
108
+ * `https://someuser@github.com/owner/repo.git` keyed on the user and so failed
109
+ * to group with the same project's ssh clone.
110
+ */
111
+ export async function originOf(path: string): Promise<string | null> {
112
+ const result = await $`git -C ${path} remote get-url origin`
113
+ .env(gitEnv())
114
+ .quiet()
115
+ .nothrow()
116
+
117
+ if (result.exitCode !== 0) return null
118
+
119
+ const raw = result.stdout.toString().trim()
120
+ if (raw.length === 0) return null
121
+
122
+ return raw
123
+ .replace(/^[a-z+]+:\/\//, '')
124
+ .replace(/^[^/@]*@/, '')
125
+ .replace(/:/, '/')
126
+ .replace(/\.git$/, '')
127
+ .replace(/\/$/, '')
128
+ .toLowerCase()
129
+ }
130
+
131
+ /**
132
+ * Walks the given roots for installed targets and reports what bounds the walk.
133
+ *
134
+ * A stamped folder is descended into like any other, because a stamp is written
135
+ * by an install someone ran in that folder and a target can hold others. The
136
+ * machine this was measured on has exactly that shape: one stamped repository
137
+ * holds the eight the hand census walked, so stopping at the outer one would
138
+ * have hidden every target the sweep exists to find.
139
+ */
140
+ export async function sweepTargets(
141
+ roots: readonly string[],
142
+ opts: SweepOptions = {},
143
+ ): Promise<SweepReport> {
144
+ const depth = opts.depth ?? DEFAULT_DEPTH
145
+ const resolveOrigin = opts.originOf ?? originOf
146
+
147
+ const found: string[] = []
148
+ const truncated: string[] = []
149
+ const unreadable: string[] = []
150
+ const symlinks: string[] = []
151
+ const seen = new Set<string>()
152
+
153
+ const walk = (dir: string, level: number): void => {
154
+ if (seen.has(dir)) return
155
+ seen.add(dir)
156
+
157
+ if (isStamped(dir)) found.push(dir)
158
+
159
+ if (level >= depth) {
160
+ truncated.push(dir)
161
+ return
162
+ }
163
+
164
+ let entries: string[]
165
+ try {
166
+ const listed = readdirSync(dir, { withFileTypes: true }).filter(
167
+ (entry) => !SKIP.has(entry.name),
168
+ )
169
+
170
+ // A symlink is reported as neither a directory nor walked, so a directory
171
+ // symlink is named here rather than dropped. Following one is available,
172
+ // since `seen` already closes the cycle, and naming it is the answer the
173
+ // bound asks for: what the walk did not reach, stated rather than
174
+ // omitted. `isDirectory` follows the link, so a symlink to a file or a
175
+ // broken one is excluded rather than read as an unfollowed directory.
176
+ for (const entry of listed) {
177
+ if (!entry.isSymbolicLink()) continue
178
+ const full = join(dir, entry.name)
179
+ if (isDirectory(full)) symlinks.push(full)
180
+ }
181
+
182
+ entries = listed
183
+ .filter((entry) => entry.isDirectory())
184
+ .map((entry) => entry.name)
185
+ } catch {
186
+ unreadable.push(dir)
187
+ return
188
+ }
189
+
190
+ for (const name of entries) walk(join(dir, name), level + 1)
191
+ }
192
+
193
+ const resolved = roots.map((root) => resolve(root))
194
+
195
+ for (const root of resolved) {
196
+ if (!isDirectory(root)) {
197
+ unreadable.push(root)
198
+ continue
199
+ }
200
+ walk(root, 0)
201
+ }
202
+
203
+ return {
204
+ targets: await group(found.sort(), resolveOrigin),
205
+ bound: { roots: resolved, depth, truncated, unreadable, symlinks },
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Collapses the paths sharing one origin into a single target.
211
+ *
212
+ * A path whose origin does not resolve stays on its own, since two checkouts
213
+ * with no remote cannot be shown to be the same project and merging them on
214
+ * that absence would undercount in the other direction.
215
+ */
216
+ async function group(
217
+ paths: readonly string[],
218
+ resolveOrigin: (path: string) => Promise<string | null>,
219
+ ): Promise<readonly SweptTarget[]> {
220
+ const origins = await Promise.all(paths.map(resolveOrigin))
221
+ const byOrigin = new Map<string, string[]>()
222
+ const alone: SweptTarget[] = []
223
+
224
+ paths.forEach((path, index) => {
225
+ const origin = origins[index]
226
+
227
+ if (origin === null || origin === undefined) {
228
+ alone.push({ paths: [path], origin: null, legacy: isLegacyStamped(path) })
229
+ return
230
+ }
231
+
232
+ const group = byOrigin.get(origin)
233
+ if (group === undefined) byOrigin.set(origin, [path])
234
+ else group.push(path)
235
+ })
236
+
237
+ const merged = [...byOrigin.entries()].map(([origin, group]) => ({
238
+ paths: group,
239
+ origin,
240
+ legacy: group.every((path) => isLegacyStamped(path)),
241
+ }))
242
+
243
+ return [...merged, ...alone].sort((a, b) =>
244
+ (a.paths[0] ?? '').localeCompare(b.paths[0] ?? ''),
245
+ )
246
+ }
@@ -58,7 +58,7 @@ Fix the feedback CLI so it applies the `feedback` label.
58
58
 
59
59
  ## Details
60
60
 
61
- `aitk feedback --github` opens an issue with no label, so `toolkit-triage` never lists it.
61
+ `aitk feedback --github` opens an issue with no label, so `aitk-feedback-triage` never lists it.
62
62
 
63
63
  ## Context
64
64