@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
|
@@ -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
|
+
}
|
package/standards/issue.md
CHANGED
|
@@ -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 `
|
|
61
|
+
`aitk feedback --github` opens an issue with no label, so `aitk-feedback-triage` never lists it.
|
|
62
62
|
|
|
63
63
|
## Context
|
|
64
64
|
|
|
File without changes
|