@erclx/aitk 3.52.1 → 3.54.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/claude-pr-review/SKILL.md +8 -0
- package/docs/agents/commands.md +1 -0
- package/docs/agents/index.md +1 -0
- package/docs/agents/key-changes.md +103 -0
- package/docs/agents/sandbox.md +11 -7
- package/docs/operating-model.md +8 -0
- package/package.json +1 -1
- package/src/cli.ts +3 -0
- package/src/commands/pr.ts +411 -0
- package/src/commands/sandbox.ts +22 -5
- package/src/pr/bijection.ts +145 -0
- package/src/pr/paths.ts +335 -0
- package/src/sandbox/expect.ts +97 -7
package/src/sandbox/expect.ts
CHANGED
|
@@ -19,6 +19,13 @@ export interface Expectation {
|
|
|
19
19
|
readonly absent: readonly string[]
|
|
20
20
|
readonly content: readonly ContentAssertion[]
|
|
21
21
|
readonly writeScope: readonly string[]
|
|
22
|
+
/**
|
|
23
|
+
* Undefined means the arm makes no claim about escapes, which is every arm
|
|
24
|
+
* today. Present, even as `[]`, means the arm asserts a bound: the empty
|
|
25
|
+
* form declares that a correct run produces none, so `stringArray`'s
|
|
26
|
+
* collapse of "absent" and "empty" into one `[]` would erase that claim.
|
|
27
|
+
*/
|
|
28
|
+
readonly escapeScope?: readonly string[]
|
|
22
29
|
readonly reply: readonly string[]
|
|
23
30
|
readonly manual: readonly string[]
|
|
24
31
|
readonly maxTurns?: number
|
|
@@ -54,15 +61,23 @@ export interface Verdict {
|
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
/**
|
|
57
|
-
* `writes` and `envelope` are absent when the caller supplied no
|
|
58
|
-
* which is not the same as a run that wrote nothing
|
|
59
|
-
* assertion kinds that depend on them report
|
|
60
|
-
* dropping out of the count, so the cheap
|
|
61
|
-
* coverage than it had.
|
|
64
|
+
* `writes`, `escapes`, and `envelope` are absent when the caller supplied no
|
|
65
|
+
* data for them, which is not the same as a run that wrote nothing, escaped
|
|
66
|
+
* nowhere, or reported nothing. The assertion kinds that depend on them report
|
|
67
|
+
* as skipped rather than silently dropping out of the count, so the cheap
|
|
68
|
+
* standalone path cannot claim more coverage than it had.
|
|
62
69
|
*/
|
|
63
70
|
export interface CheckInput {
|
|
64
71
|
readonly sandboxDir: string
|
|
65
72
|
readonly writes?: readonly string[]
|
|
73
|
+
readonly escapes?: readonly string[]
|
|
74
|
+
/**
|
|
75
|
+
* Whether any watched escape root held one of the four directories this run.
|
|
76
|
+
* Undefined when the caller supplied no escapes at all, which already skips.
|
|
77
|
+
* False is what separates a watch that ran and found nothing from one with
|
|
78
|
+
* nothing to watch, both of which produce the same empty `escapes` list.
|
|
79
|
+
*/
|
|
80
|
+
readonly escapesWatched?: boolean
|
|
66
81
|
readonly envelope?: RunEnvelope
|
|
67
82
|
}
|
|
68
83
|
|
|
@@ -187,6 +202,10 @@ export function parseExpectation(source: string): Expectation {
|
|
|
187
202
|
absent: stringArray(parsed.absent),
|
|
188
203
|
content: contentArray(parsed.content),
|
|
189
204
|
writeScope: stringArray(parsed.write_scope),
|
|
205
|
+
escapeScope:
|
|
206
|
+
parsed.escape_scope === undefined
|
|
207
|
+
? undefined
|
|
208
|
+
: stringArray(parsed.escape_scope),
|
|
190
209
|
reply: stringArray(parsed.reply),
|
|
191
210
|
manual: stringArray(parsed.manual),
|
|
192
211
|
maxTurns:
|
|
@@ -205,6 +224,7 @@ export function countMechanicalAssertions(expectation: Expectation): number {
|
|
|
205
224
|
expectation.absent.length +
|
|
206
225
|
expectation.content.length +
|
|
207
226
|
expectation.writeScope.length +
|
|
227
|
+
(expectation.escapeScope === undefined ? 0 : 1) +
|
|
208
228
|
expectation.reply.length
|
|
209
229
|
)
|
|
210
230
|
}
|
|
@@ -326,7 +346,7 @@ function checkWriteScope(
|
|
|
326
346
|
// none, and without this the declaration vanishes from the verdict entirely:
|
|
327
347
|
// no result, no skipped entry, and no contribution to the unchecked count that
|
|
328
348
|
// exists to surface exactly this. The `undefined` branch above cannot stand in,
|
|
329
|
-
// since `run.sh` always passes `--writes` and `
|
|
349
|
+
// since `run.sh` always passes `--writes` and `readPathList` returns `[]` for an
|
|
330
350
|
// empty file. An arm whose output escaped the snapshot reads as a clean run,
|
|
331
351
|
// which is the vacuous pass the harness exists to remove.
|
|
332
352
|
if (writes.length === 0) {
|
|
@@ -348,6 +368,65 @@ function checkWriteScope(
|
|
|
348
368
|
}
|
|
349
369
|
}
|
|
350
370
|
|
|
371
|
+
/**
|
|
372
|
+
* `run.sh` watches two toolkit roots for a write to shared session scratch
|
|
373
|
+
* during a run, and reports every one it finds as an unattributed escape with
|
|
374
|
+
* no arm able to fail on it. An arm whose skill legitimately reaches outside
|
|
375
|
+
* the sandbox tree declares the destinations here, and the harness asserts
|
|
376
|
+
* them instead of trusting the skill to bound itself.
|
|
377
|
+
*
|
|
378
|
+
* Declaring the scope inverts the empty case against `checkWriteScope`. A
|
|
379
|
+
* write-scope declaration exists to bound required output, so a run that wrote
|
|
380
|
+
* nothing skips rather than passing on a fabricated zero. An escape-scope
|
|
381
|
+
* declaration exists to bound a side effect nothing requires, so zero escapes
|
|
382
|
+
* is the outcome a correct run produces and reports as a pass outright,
|
|
383
|
+
* provided a watched root held something to watch. `run.sh`'s `snapshot_root`
|
|
384
|
+
* returns an empty manifest both when a watch ran clean and when none of the
|
|
385
|
+
* four watched directories existed under a root, and the two produce the same
|
|
386
|
+
* empty `escapes` list. `watched` is what tells them apart: a run that had
|
|
387
|
+
* nothing to watch reports unmeasured rather than passing on a diff it never
|
|
388
|
+
* had the target to take.
|
|
389
|
+
*/
|
|
390
|
+
function checkEscapeScope(
|
|
391
|
+
expectation: Expectation,
|
|
392
|
+
escapes: readonly string[] | undefined,
|
|
393
|
+
watched: boolean | undefined,
|
|
394
|
+
): KindOutcome {
|
|
395
|
+
if (expectation.escapeScope === undefined) return { results: [], skipped: [] }
|
|
396
|
+
|
|
397
|
+
if (escapes === undefined) {
|
|
398
|
+
return {
|
|
399
|
+
results: [],
|
|
400
|
+
skipped: ['escape scope: no escape data supplied, pass --escapes'],
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (escapes.length === 0) {
|
|
405
|
+
if (watched === false) {
|
|
406
|
+
return {
|
|
407
|
+
results: [],
|
|
408
|
+
skipped: ['escape scope: no watched root held a target, unmeasured'],
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
results: [{ ok: true, message: 'no escape during this run' }],
|
|
414
|
+
skipped: [],
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const globs = expectation.escapeScope.map((glob) => new Bun.Glob(glob))
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
results: escapes.map((path) =>
|
|
422
|
+
globs.some((glob) => glob.match(path))
|
|
423
|
+
? { ok: true, message: `declared escape: ${path}` }
|
|
424
|
+
: { ok: false, message: `unbounded escape: ${path}` },
|
|
425
|
+
),
|
|
426
|
+
skipped: [],
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
351
430
|
/**
|
|
352
431
|
* Plain substrings, matched case-sensitively, against the text the run replied
|
|
353
432
|
* with. A substring rather than a regex because the pattern a reply assertion
|
|
@@ -431,6 +510,11 @@ export function checkExpectation(
|
|
|
431
510
|
input: CheckInput,
|
|
432
511
|
): Verdict {
|
|
433
512
|
const scope = checkWriteScope(expectation, input.writes)
|
|
513
|
+
const escapeScope = checkEscapeScope(
|
|
514
|
+
expectation,
|
|
515
|
+
input.escapes,
|
|
516
|
+
input.escapesWatched,
|
|
517
|
+
)
|
|
434
518
|
const reply = checkReply(expectation, input.envelope)
|
|
435
519
|
const envelope = checkEnvelope(expectation, input.envelope)
|
|
436
520
|
|
|
@@ -440,9 +524,15 @@ export function checkExpectation(
|
|
|
440
524
|
...checkContent(expectation, input.sandboxDir),
|
|
441
525
|
...reply.results,
|
|
442
526
|
...scope.results,
|
|
527
|
+
...escapeScope.results,
|
|
443
528
|
...envelope.results,
|
|
444
529
|
]
|
|
445
|
-
const skipped = [
|
|
530
|
+
const skipped = [
|
|
531
|
+
...scope.skipped,
|
|
532
|
+
...escapeScope.skipped,
|
|
533
|
+
...reply.skipped,
|
|
534
|
+
...envelope.skipped,
|
|
535
|
+
]
|
|
446
536
|
|
|
447
537
|
const failed = results.filter((result) => !result.ok).length
|
|
448
538
|
|