@erclx/aitk 3.51.1 → 3.52.1
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/claude-orchestrate/scripts/watch.sh +74 -3
- package/docs/agents/audits.md +1 -1
- package/docs/agents/commands.md +2 -0
- package/docs/agents/gate.md +84 -0
- package/docs/agents/index.md +1 -0
- package/docs/agents/markdown-audit.md +3 -3
- package/docs/agents/sessions.md +8 -0
- package/docs/agents/state-scoped-risk.md +1 -1
- package/package.json +3 -3
- package/scripts/core/list-seed-roots.sh +18 -0
- package/scripts/core/repair-bare-flag.sh +19 -0
- package/scripts/core/update.sh +3 -3
- package/src/audits/catalog.ts +2 -2
- package/src/audits/run.ts +2 -1
- package/src/cli.ts +4 -0
- package/src/commands/claude.ts +2 -2
- package/src/commands/context.ts +2 -2
- package/src/commands/gate.ts +189 -0
- package/src/commands/sessions.ts +27 -1
- package/src/gate/measures.ts +682 -0
- package/src/gate/sequencer.ts +386 -0
- package/src/gate/stages.ts +412 -0
- package/src/sessions/registry.ts +17 -0
- package/src/sessions/resolve.ts +44 -2
- package/scripts/core/verify.sh +0 -694
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import {
|
|
3
|
+
cliRunner,
|
|
4
|
+
collectChangedFiles,
|
|
5
|
+
commandRunner,
|
|
6
|
+
exitCodeFor,
|
|
7
|
+
type GateContext,
|
|
8
|
+
repairBareFlag,
|
|
9
|
+
type StageResult,
|
|
10
|
+
type Summary,
|
|
11
|
+
runStages,
|
|
12
|
+
summarize,
|
|
13
|
+
} from '@/gate/sequencer'
|
|
14
|
+
import { STAGES } from '@/gate/stages'
|
|
15
|
+
import { PROJECT_ROOT } from '@/project-root'
|
|
16
|
+
import {
|
|
17
|
+
intro,
|
|
18
|
+
logError,
|
|
19
|
+
logInfo,
|
|
20
|
+
logStep,
|
|
21
|
+
logWarn,
|
|
22
|
+
outro,
|
|
23
|
+
palette,
|
|
24
|
+
pipeOutput,
|
|
25
|
+
plural,
|
|
26
|
+
} from '@/ui'
|
|
27
|
+
|
|
28
|
+
interface RunCommandOptions {
|
|
29
|
+
readonly all?: boolean
|
|
30
|
+
readonly write?: boolean
|
|
31
|
+
readonly nested?: boolean
|
|
32
|
+
readonly json?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function register(program: Command): void {
|
|
36
|
+
const gate = program
|
|
37
|
+
.command('gate')
|
|
38
|
+
.description('Run the merge gate this repository verifies a branch with')
|
|
39
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
40
|
+
|
|
41
|
+
gate
|
|
42
|
+
.command('run')
|
|
43
|
+
.description(
|
|
44
|
+
'Run every gating stage in order, scoping shell, types, and tests to the changed set',
|
|
45
|
+
)
|
|
46
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
47
|
+
.option(
|
|
48
|
+
'--all',
|
|
49
|
+
'Run every stage instead of scoping shell, types, and tests to changed files',
|
|
50
|
+
)
|
|
51
|
+
.option(
|
|
52
|
+
'--no-write',
|
|
53
|
+
'Check formatting instead of applying it, which is what a merge gate wants',
|
|
54
|
+
)
|
|
55
|
+
.option(
|
|
56
|
+
'--nested',
|
|
57
|
+
'Suppress the outer frame when another script opened one',
|
|
58
|
+
)
|
|
59
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
60
|
+
.addHelpText(
|
|
61
|
+
'after',
|
|
62
|
+
[
|
|
63
|
+
'',
|
|
64
|
+
'Exit codes:',
|
|
65
|
+
' 0 every stage that ran reported, and none found a fact',
|
|
66
|
+
' 1 a stage found a fact, or could not measure its input under CI',
|
|
67
|
+
'',
|
|
68
|
+
'A stage halts the run, so clearing a regenerate-then-assert stage',
|
|
69
|
+
'reveals the next one behind it rather than the whole set at once.',
|
|
70
|
+
'',
|
|
71
|
+
'A stage that cannot read its input reports rather than passing. On a',
|
|
72
|
+
"contributor's machine that is a warning and the run still exits 0,",
|
|
73
|
+
'because an absent tool there is somebody mid-setup. Under CI it',
|
|
74
|
+
'refuses, because the same absence is a broken workflow step and a',
|
|
75
|
+
'green run over a stage that measured nothing is the pass the gate',
|
|
76
|
+
'exists to withhold.',
|
|
77
|
+
'',
|
|
78
|
+
'Examples:',
|
|
79
|
+
' aitk gate run',
|
|
80
|
+
' aitk gate run --all --no-write',
|
|
81
|
+
' aitk gate run --json',
|
|
82
|
+
'',
|
|
83
|
+
].join('\n'),
|
|
84
|
+
)
|
|
85
|
+
.action(async (opts: RunCommandOptions) => {
|
|
86
|
+
process.exitCode = await runGate(opts)
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function runGate(opts: RunCommandOptions): Promise<number> {
|
|
91
|
+
const root = PROJECT_ROOT
|
|
92
|
+
const emitJson = opts.json ?? false
|
|
93
|
+
const nested = opts.nested ?? false
|
|
94
|
+
const write = opts.write ?? true
|
|
95
|
+
const run = commandRunner(root)
|
|
96
|
+
|
|
97
|
+
if (!emitJson && !nested) intro('aitk gate run')
|
|
98
|
+
|
|
99
|
+
await repairBareFlag(root)
|
|
100
|
+
|
|
101
|
+
const changed = opts.all
|
|
102
|
+
? { scoped: false, files: [] }
|
|
103
|
+
: await collectChangedFiles(run)
|
|
104
|
+
if (!emitJson && changed.notice !== undefined) logWarn(changed.notice)
|
|
105
|
+
|
|
106
|
+
const ctx: GateContext = {
|
|
107
|
+
root,
|
|
108
|
+
ci: process.env.CI === 'true',
|
|
109
|
+
run,
|
|
110
|
+
cli: cliRunner(root),
|
|
111
|
+
write,
|
|
112
|
+
changed: changed.scoped ? changed.files : undefined,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const results = await runStages(
|
|
116
|
+
STAGES,
|
|
117
|
+
ctx,
|
|
118
|
+
emitJson ? undefined : (result) => report(result),
|
|
119
|
+
)
|
|
120
|
+
const summary = summarize(results)
|
|
121
|
+
const code = exitCodeFor(results)
|
|
122
|
+
|
|
123
|
+
if (emitJson) {
|
|
124
|
+
const failed = results.find((result) => result.status === 'failed')
|
|
125
|
+
// Diagnostics reach stderr in every mode, so a caller reading the record
|
|
126
|
+
// alone is not the only one told what went wrong.
|
|
127
|
+
if (failed?.failure !== undefined) {
|
|
128
|
+
process.stderr.write(`${failed.label}: ${failed.failure}\n`)
|
|
129
|
+
}
|
|
130
|
+
process.stdout.write(
|
|
131
|
+
`${JSON.stringify({
|
|
132
|
+
ok: code === 0,
|
|
133
|
+
root,
|
|
134
|
+
scoped: changed.scoped,
|
|
135
|
+
changed: changed.files.length,
|
|
136
|
+
summary,
|
|
137
|
+
stages: results.map(({ id, label, status, failure }) => ({
|
|
138
|
+
id,
|
|
139
|
+
label,
|
|
140
|
+
status,
|
|
141
|
+
failure,
|
|
142
|
+
})),
|
|
143
|
+
})}\n`,
|
|
144
|
+
)
|
|
145
|
+
return code
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!nested) close(summary, code)
|
|
149
|
+
return code
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function report(result: StageResult): void {
|
|
153
|
+
logStep(result.label)
|
|
154
|
+
for (const emission of result.emissions) {
|
|
155
|
+
if (emission.kind === 'info') logInfo(emission.text)
|
|
156
|
+
else if (emission.kind === 'warn') logWarn(emission.text)
|
|
157
|
+
else pipeOutput(emission.text)
|
|
158
|
+
}
|
|
159
|
+
if (result.failure !== undefined) logError(result.failure)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The closing verdict, which names what the run did not measure rather than
|
|
164
|
+
* reporting a bare pass over it.
|
|
165
|
+
*
|
|
166
|
+
* A run where every stage read its input still closes on the line the script
|
|
167
|
+
* this replaces closed on, so nothing about the ordinary case moved. A run
|
|
168
|
+
* carrying an unmeasured stage says so, because a green line over a stage that
|
|
169
|
+
* looked at nothing is exactly the silence the reporting outcome exists
|
|
170
|
+
* against.
|
|
171
|
+
*/
|
|
172
|
+
function close(summary: Summary, code: number): void {
|
|
173
|
+
const { GREEN, NC, RED, YELLOW } = palette(process.stderr)
|
|
174
|
+
outro()
|
|
175
|
+
|
|
176
|
+
if (code !== 0) {
|
|
177
|
+
process.stderr.write(`${RED}✗ Verification failed${NC}\n\n`)
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (summary.unmeasured > 0) {
|
|
182
|
+
process.stderr.write(
|
|
183
|
+
`${YELLOW}! Verification passed, ${plural(summary.unmeasured, 'stage')} measured nothing${NC}\n\n`,
|
|
184
|
+
)
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
process.stderr.write(`${GREEN}✓ Verification passed${NC}\n\n`)
|
|
189
|
+
}
|
package/src/commands/sessions.ts
CHANGED
|
@@ -124,6 +124,14 @@ export function register(program: Command): void {
|
|
|
124
124
|
'cannot rule out a pid handed to an unrelated process, so a roster',
|
|
125
125
|
'reported that way is a candidate list rather than an identity.',
|
|
126
126
|
'',
|
|
127
|
+
'The JSON record also carries "statusUpdatedAt" (the stamp the client',
|
|
128
|
+
'wrote beside "status", or null where its record carries none) and',
|
|
129
|
+
'"statusDwellMs" (the elapsed milliseconds since that stamp, falling',
|
|
130
|
+
'back to the coarser "updatedAt" where the narrower one is absent,',
|
|
131
|
+
'computed at read time and clamped at zero against clock skew). The',
|
|
132
|
+
'framed listing renders the same dwell beside the status, at the',
|
|
133
|
+
'coarsest unit that keeps it a whole number.',
|
|
134
|
+
'',
|
|
127
135
|
'Examples:',
|
|
128
136
|
' aitk sessions list',
|
|
129
137
|
' aitk sessions list --json',
|
|
@@ -349,12 +357,30 @@ function reportSessions(
|
|
|
349
357
|
const held =
|
|
350
358
|
session.branch ??
|
|
351
359
|
`unresolved: ${REASONS[session.unresolved ?? ''] ?? 'unknown'}`
|
|
352
|
-
|
|
360
|
+
const dwell = formatDwell(session.statusDwellMs)
|
|
361
|
+
const status = dwell ? `${session.status} ${dwell}` : session.status
|
|
362
|
+
return `${session.name} ${status} ${held}\n ${session.cwd}`
|
|
353
363
|
})
|
|
354
364
|
.join('\n'),
|
|
355
365
|
)
|
|
356
366
|
}
|
|
357
367
|
|
|
368
|
+
/**
|
|
369
|
+
* Renders the dwell at the coarsest unit that keeps it a whole number, since a
|
|
370
|
+
* reader scanning a roster wants an age at a glance rather than a millisecond
|
|
371
|
+
* count. An absent dwell renders as nothing, folding a status carrying no
|
|
372
|
+
* stamp back to the bare status line the reader already knew.
|
|
373
|
+
*/
|
|
374
|
+
function formatDwell(ms: number | null): string {
|
|
375
|
+
if (ms === null) return ''
|
|
376
|
+
const seconds = Math.round(ms / 1000)
|
|
377
|
+
if (seconds < 60) return `${seconds}s`
|
|
378
|
+
const minutes = Math.round(seconds / 60)
|
|
379
|
+
if (minutes < 60) return `${minutes}m`
|
|
380
|
+
const hours = Math.round(minutes / 60)
|
|
381
|
+
return `${hours}h`
|
|
382
|
+
}
|
|
383
|
+
|
|
358
384
|
function reportClaim(claim: ClaimReport): void {
|
|
359
385
|
logStep('Claim')
|
|
360
386
|
|