agent-afk 3.80.6 → 3.81.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/README.md +6 -10
- package/dist/bundled-plugins/awa-bundled/skills/contract/SKILL.md +1 -1
- package/dist/bundled-plugins/awa-bundled/skills/ground-claim/SKILL.md +3 -3
- package/dist/cli.mjs +373 -373
- package/dist/index.mjs +130 -127
- package/dist/telegram.mjs +141 -138
- package/package.json +1 -2
- package/dist/bundled-plugins/awa-bundled/bundled.test.ts +0 -403
- package/dist/threads.mjs +0 -1366
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-afk",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.81.0",
|
|
4
4
|
"description": "CLI tool for interacting with AI agents via multiple interfaces",
|
|
5
5
|
"main": "dist/index.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -89,7 +89,6 @@
|
|
|
89
89
|
"test": "vitest run",
|
|
90
90
|
"test:coverage": "vitest run --coverage",
|
|
91
91
|
"test:watch": "vitest watch",
|
|
92
|
-
"test:landing": "cd landing && npm test",
|
|
93
92
|
"lint": "tsc --noEmit",
|
|
94
93
|
"audit:sdk": "tsx scripts/audit-sdk-dependency.ts",
|
|
95
94
|
"audit:sdk:check": "tsx scripts/audit-sdk-dependency.ts --check",
|
|
@@ -1,403 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
3
|
-
import { createHash } from 'node:crypto';
|
|
4
|
-
import { join, dirname } from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
|
|
7
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
-
const __dirname = dirname(__filename);
|
|
9
|
-
|
|
10
|
-
// Pinned hashes for the 12 bundled skills shipped under awa-bundled/. These
|
|
11
|
-
// files mirror — but are NOT byte-equal to — corresponding skills in the
|
|
12
|
-
// upstream awa-private repo. Permanent intentional differences include:
|
|
13
|
-
//
|
|
14
|
-
// - Namespace prefixes (`/awa-dev:contract` upstream → `/contract` here)
|
|
15
|
-
// - Sub-agent dispatch identifiers (`awa-private:research-agent` → `research-agent`)
|
|
16
|
-
// - Occasional wording divergence between maintainers
|
|
17
|
-
//
|
|
18
|
-
// Because byte-equality is a false invariant, this file enforces only the
|
|
19
|
-
// pinned-hash snapshot: any unauthored edit to a bundled SKILL.md fails the
|
|
20
|
-
// test until the developer explicitly bumps the hash here. That bump is the
|
|
21
|
-
// forcing function for cross-repo discipline:
|
|
22
|
-
//
|
|
23
|
-
// *** Workflow when bumping a pinned hash ***
|
|
24
|
-
// 1. Identify what changed in the bundled SKILL.md.
|
|
25
|
-
// 2. Check whether the same change applies upstream in
|
|
26
|
-
// awa-private/plugins/{awa-dev,awa-private}/skills/<name>/SKILL.md.
|
|
27
|
-
// 3. If yes → open a parallel PR in awa-private. Land both before either
|
|
28
|
-
// is released.
|
|
29
|
-
// 4. If no → document why the change is bundled-only in the PR description.
|
|
30
|
-
// 5. Only then update the hash below.
|
|
31
|
-
//
|
|
32
|
-
// This convention exists because in November 2026 a critical /ship guardrail
|
|
33
|
-
// (the "Branch lock" + "Never push to main" Hard Rules in commit 63f3ed3)
|
|
34
|
-
// was added to the bundled mirror but never back-ported to awa-private. The
|
|
35
|
-
// deployed plugin therefore lacked the guardrail until the next sync. This
|
|
36
|
-
// test cannot prevent that on its own — but the hash-bump moment forces the
|
|
37
|
-
// developer to look at both copies.
|
|
38
|
-
const PINNED_HASHES = {
|
|
39
|
-
contract: '2c8a3779f225902f2a8b0af74bfc66c1cdbae58f863d1205c66ce44a14e275b5',
|
|
40
|
-
'devils-advocate':
|
|
41
|
-
'84275b097fa3ed270b0b71c87e2dad0366794fd7efc7a47d29abaa85da97f974',
|
|
42
|
-
gather: 'ec2964fb1f47970fffba6bafacb4dc4f0c76291a7cc0da92ff069a0a986decb4',
|
|
43
|
-
'ground-claim':
|
|
44
|
-
'64a4fa0b63467a9a7ae6e61afd68813ff59bfb46a8c5e072feafa15473e36f2a',
|
|
45
|
-
'ground-state':
|
|
46
|
-
'ae4c167296e96b640a54cd4cd317e5810894cffff6dac3c022b1433dff003105',
|
|
47
|
-
// intent-lock is bundled-only (not present in upstream awa-private or
|
|
48
|
-
// awa-dev). Hash bumps need no parallel PR — document the change in the
|
|
49
|
-
// commit message instead.
|
|
50
|
-
'intent-lock':
|
|
51
|
-
'7a466075e5a64c1145b97aa24b9a6990a3ee1dc818b93c158433e53d7416aef0',
|
|
52
|
-
parallelize:
|
|
53
|
-
'74b1a7cf866d630dce0d33323663a8b818f149b5f4d4ef60feba1aeb3472e49b',
|
|
54
|
-
// refactor is bundled-only (no upstream awa-private counterpart); verbatim
|
|
55
|
-
// copy of the user-scope /refactor at ~/.afk/skills/.
|
|
56
|
-
refactor: '23ab4836653159deeafbca45e516af8d43e8c5275535613e36f7bcb2d77de64e',
|
|
57
|
-
research: '0d04d0a05891ed1b63679e5a0237b743364a6165731a8f694c5584ed7661505f',
|
|
58
|
-
review: '816ea27cf665be23c67cf887d639d40e1435954f80ceeb43740bcd7f39c205e7',
|
|
59
|
-
'shadow-verify':
|
|
60
|
-
'8bce741e55be049a196ed6c71efd0acd271f272a8e2202917c3f1243b875eb33',
|
|
61
|
-
ship: '4b9a0e40372c36f953ad6d37347e1682950c9825ca5e312fae4e9b320cde975f',
|
|
62
|
-
// simplify is bundled-only (no upstream awa-private counterpart).
|
|
63
|
-
simplify:
|
|
64
|
-
'b863890eead7011c90d4f93b65e5a1533c8f88292728ec771f8b128e9535d996',
|
|
65
|
-
spec: 'c08f3b4fbe1f585b1e8354a000e0d2d3a48455ad322c7a27112d509aa9698fe7',
|
|
66
|
-
} as const;
|
|
67
|
-
|
|
68
|
-
type SkillName = keyof typeof PINNED_HASHES;
|
|
69
|
-
|
|
70
|
-
const SKILLS = Object.keys(PINNED_HASHES) as SkillName[];
|
|
71
|
-
|
|
72
|
-
// ── Namespace-normalized drift detection ──────────────────────────────────────
|
|
73
|
-
//
|
|
74
|
-
// Workspace root is four levels above __dirname (src/bundled-plugins/awa-bundled).
|
|
75
|
-
// awa-private is a sibling of agent-afk at the workspace root level.
|
|
76
|
-
// This mirrors the pattern used in src/skills/_agents/vendored.test.ts.
|
|
77
|
-
const WORKSPACE_ROOT = join(__dirname, '../../../..');
|
|
78
|
-
|
|
79
|
-
// Upstream source paths relative to WORKSPACE_ROOT.
|
|
80
|
-
// intent-lock is bundled-only — no upstream comparison row.
|
|
81
|
-
const UPSTREAM_PATHS: Partial<Record<SkillName, string>> = {
|
|
82
|
-
contract: 'awa-private/plugins/awa-dev/skills/contract/SKILL.md',
|
|
83
|
-
gather: 'awa-private/plugins/awa-dev/skills/gather/SKILL.md',
|
|
84
|
-
'ground-claim': 'awa-private/plugins/awa-dev/skills/ground-claim/SKILL.md',
|
|
85
|
-
'ground-state': 'awa-private/plugins/awa-dev/skills/ground-state/SKILL.md',
|
|
86
|
-
research: 'awa-private/plugins/awa-dev/skills/research/SKILL.md',
|
|
87
|
-
ship: 'awa-private/plugins/awa-dev/skills/ship/SKILL.md',
|
|
88
|
-
spec: 'awa-private/plugins/awa-dev/skills/spec/SKILL.md',
|
|
89
|
-
'devils-advocate':
|
|
90
|
-
'awa-private/plugins/awa-private/skills/devils-advocate/SKILL.md',
|
|
91
|
-
parallelize: 'awa-private/plugins/awa-private/skills/parallelize/SKILL.md',
|
|
92
|
-
review: 'awa-private/plugins/awa-private/skills/review/SKILL.md',
|
|
93
|
-
'shadow-verify':
|
|
94
|
-
'awa-private/plugins/awa-private/skills/shadow-verify/SKILL.md',
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
// Normalize both copies before comparing, removing all permanent intentional
|
|
98
|
-
// namespace shifts:
|
|
99
|
-
//
|
|
100
|
-
// /awa-dev:contract → /contract
|
|
101
|
-
// /awa-private:ship → /ship
|
|
102
|
-
// `awa-dev:ground-state` → `ground-state`
|
|
103
|
-
// "awa-private:research-agent" → "research-agent"
|
|
104
|
-
//
|
|
105
|
-
// After normalization, any remaining diff is either real drift (a change
|
|
106
|
-
// landed in one mirror but not the other) or an explicitly allowlisted
|
|
107
|
-
// intentional divergence documented in INTENTIONAL_DIFFS below.
|
|
108
|
-
function normalize(content: string): string {
|
|
109
|
-
return content
|
|
110
|
-
.replace(/\/awa-dev:/g, '/')
|
|
111
|
-
.replace(/\/awa-private:/g, '/')
|
|
112
|
-
.replace(/`awa-dev:/g, '`')
|
|
113
|
-
.replace(/`awa-private:/g, '`')
|
|
114
|
-
.replace(/"awa-dev:/g, '"')
|
|
115
|
-
.replace(/"awa-private:/g, '"');
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// INTENTIONAL_DIFFS: per-skill array of RegExp patterns. A normalized diff
|
|
119
|
-
// line matching any pattern for that skill is silently accepted — the line is
|
|
120
|
-
// removed from BOTH sides before comparison (each pattern is applied to both
|
|
121
|
-
// the bundled and upstream line arrays independently).
|
|
122
|
-
//
|
|
123
|
-
// *** Adding an entry here requires an inline comment justifying why the
|
|
124
|
-
// divergence is intentional. "It seems fine" is NOT sufficient — if you
|
|
125
|
-
// cannot defensibly justify it, surface it as unclassified drift in the PR
|
|
126
|
-
// body instead. ***
|
|
127
|
-
const INTENTIONAL_DIFFS: Partial<Record<SkillName, RegExp[]>> = {
|
|
128
|
-
// devils-advocate, parallelize, shadow-verify:
|
|
129
|
-
// Both sides contain a "Sub-agent contract" invocation line immediately
|
|
130
|
-
// after the frontmatter block, but they use different plugin namespaces:
|
|
131
|
-
//
|
|
132
|
-
// Bundled: /contract (resolves to the co-bundled contract skill)
|
|
133
|
-
// Upstream: /agent-workflow-amplifiers:contract (third-party plugin ns)
|
|
134
|
-
//
|
|
135
|
-
// The `normalize()` function only strips `awa-dev:` and `awa-private:`
|
|
136
|
-
// prefixes; it intentionally does NOT touch `agent-workflow-amplifiers:`
|
|
137
|
-
// because that is a distinct third-party plugin, not a namespace shift of
|
|
138
|
-
// the same plugin. Both copies invoke the same logical skill — the
|
|
139
|
-
// difference is which plugin registry entry resolves the name. This is
|
|
140
|
-
// intentional structural divergence: bundled uses self-contained routing;
|
|
141
|
-
// upstream relies on the agent-workflow-amplifiers plugin being installed.
|
|
142
|
-
//
|
|
143
|
-
// Pattern rationale: we match both the bare `/contract` line (bundled side)
|
|
144
|
-
// and the namespaced `/agent-workflow-amplifiers:contract` line (upstream
|
|
145
|
-
// side) so both are removed before the equality check.
|
|
146
|
-
'devils-advocate': [
|
|
147
|
-
// Bundled side: bare /contract invocation (no plugin prefix).
|
|
148
|
-
/^\/contract$/,
|
|
149
|
-
// Upstream side: /agent-workflow-amplifiers:contract invocation.
|
|
150
|
-
/\/agent-workflow-amplifiers:contract/,
|
|
151
|
-
],
|
|
152
|
-
parallelize: [
|
|
153
|
-
// Same structural divergence as devils-advocate — different contract
|
|
154
|
-
// skill namespace on bundled vs upstream.
|
|
155
|
-
/^\/contract$/,
|
|
156
|
-
/\/agent-workflow-amplifiers:contract/,
|
|
157
|
-
],
|
|
158
|
-
'shadow-verify': [
|
|
159
|
-
// Same structural divergence as devils-advocate.
|
|
160
|
-
/^\/contract$/,
|
|
161
|
-
/\/agent-workflow-amplifiers:contract/,
|
|
162
|
-
],
|
|
163
|
-
|
|
164
|
-
// research — 1-line divergence, #441 back-port gap:
|
|
165
|
-
// "if the research-agent is not available" (bundled) vs
|
|
166
|
-
// "if the private plugin is not installed" (upstream).
|
|
167
|
-
// Bundled users have no concept of "private plugin" — the research-agent
|
|
168
|
-
// IS bundled, so "not available" is the correct user-facing phrase. The
|
|
169
|
-
// upstream wording assumed plugin-based deployment context. This divergence
|
|
170
|
-
// is intentional for bundled context; upstream should ideally adopt a
|
|
171
|
-
// context-neutral phrasing. Flagged for #441 reconciliation.
|
|
172
|
-
research: [
|
|
173
|
-
/if the research-agent is not available/,
|
|
174
|
-
/if the private plugin is not installed/,
|
|
175
|
-
],
|
|
176
|
-
|
|
177
|
-
// ship — 3 divergences, all #441 back-port gaps:
|
|
178
|
-
//
|
|
179
|
-
// 1. Phase 3 heading:
|
|
180
|
-
// Bundled: "Phase 3 — Draft commit message."
|
|
181
|
-
// Upstream: "Phase 3 — Draft commit message (user-approval gate)."
|
|
182
|
-
// The "(user-approval gate)" annotation was added in upstream but not
|
|
183
|
-
// back-ported to bundled. Both copies have the same behavior (no
|
|
184
|
-
// approval gate); the annotation is a clarifying label. Real drift,
|
|
185
|
-
// flagged for #441 back-port.
|
|
186
|
-
//
|
|
187
|
-
// 2. Phase 3 body prose:
|
|
188
|
-
// Bundled: "Print the draft message + file list to the user as
|
|
189
|
-
// info-only output, then **immediately** invoke Phase 4.
|
|
190
|
-
// **This is not a gate. Do not ask "does this look good?" Do not
|
|
191
|
-
// wait for approval.** The user surface is one continuous turn:
|
|
192
|
-
// draft → commit → push → PR URL."
|
|
193
|
-
// Upstream: "Surface the draft message + file list to the user for
|
|
194
|
-
// visibility, then proceed immediately to commit. Do not wait for
|
|
195
|
-
// approval."
|
|
196
|
-
// Upstream simplified the prose; semantics are identical. Real drift
|
|
197
|
-
// (editorial improvement in upstream not back-ported). Flagged for #441.
|
|
198
|
-
//
|
|
199
|
-
// 3. Phase 5 Never-push-main bullet order:
|
|
200
|
-
// Bundled: bullet appears after "Non-fast-forward rejection" bullet.
|
|
201
|
-
// Upstream: bullet appears before "Upstream unset" bullet (earlier).
|
|
202
|
-
// Same safety rule, different list position. Real drift (harmless
|
|
203
|
-
// reordering). Flagged for #441 back-port.
|
|
204
|
-
ship: [
|
|
205
|
-
// Heading divergence (1 above).
|
|
206
|
-
/Phase 3 — Draft commit message\./,
|
|
207
|
-
/Phase 3 — Draft commit message \(user-approval gate\)\./,
|
|
208
|
-
// Prose divergence (2 above) — match the diverging body paragraph.
|
|
209
|
-
/Print the draft message \+ file list to the user as info-only output/,
|
|
210
|
-
/then \*\*immediately\*\* invoke Phase 4\./,
|
|
211
|
-
/\*\*This is not a gate\. Do not ask "does this look good\?" Do not wait for approval\.\*\*/,
|
|
212
|
-
/The user surface is one continuous turn: draft → commit → push → PR URL\./,
|
|
213
|
-
/Surface the draft message \+ file list to the user for visibility/,
|
|
214
|
-
/then proceed immediately to commit\. Do not wait for approval\./,
|
|
215
|
-
// Bullet ordering divergence (3 above).
|
|
216
|
-
/\*\*Never\*\* `git push origin main` \(or `master`\)\. Pushing the feature branch is the only allowed form\./,
|
|
217
|
-
],
|
|
218
|
-
|
|
219
|
-
// review — namespace-only divergence (back-port landed; #441 closed):
|
|
220
|
-
// The bundled review is now the de-namespaced mirror of upstream
|
|
221
|
-
// awa-private review. The previously-allowlisted #441 drift —
|
|
222
|
-
// Wave 1.5 (citation + absence-claim verification), reviewed-ref
|
|
223
|
-
// capture / SHA pinning, the citation-requirement block, the severity
|
|
224
|
-
// sort-order block, epistemic scope disclosure, and the ref:<sha>
|
|
225
|
-
// finding-schema fields — has been back-ported into bundled; and the
|
|
226
|
-
// api-compat reachability + absence-claim grounding gates were ported
|
|
227
|
-
// the other way into upstream (griffinwork40/awa-private#40). Both
|
|
228
|
-
// copies now carry the full superset, so the only remaining divergence
|
|
229
|
-
// is the same contract-namespace shift as devils-advocate / parallelize
|
|
230
|
-
// / shadow-verify: bundled uses /contract (self-contained routing),
|
|
231
|
-
// upstream uses /agent-workflow-amplifiers:contract (third-party ns).
|
|
232
|
-
review: [
|
|
233
|
-
// Bundled side: bare /contract invocation (no plugin prefix).
|
|
234
|
-
/^\/contract$/,
|
|
235
|
-
// Upstream side: /agent-workflow-amplifiers:contract invocation.
|
|
236
|
-
/\/agent-workflow-amplifiers:contract/,
|
|
237
|
-
],
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
241
|
-
|
|
242
|
-
function computeHash(content: string): string {
|
|
243
|
-
return createHash('sha256').update(content).digest('hex');
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function bundledPath(name: SkillName): string {
|
|
247
|
-
return join(__dirname, 'skills', name, 'SKILL.md');
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function readBundled(name: SkillName): string {
|
|
251
|
-
return readFileSync(bundledPath(name), 'utf8');
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function upstreamAbsPath(name: SkillName): string | null {
|
|
255
|
-
const rel = UPSTREAM_PATHS[name];
|
|
256
|
-
if (!rel) return null;
|
|
257
|
-
return join(WORKSPACE_ROOT, rel);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function upstreamAvailable(name: SkillName): boolean {
|
|
261
|
-
const abs = upstreamAbsPath(name);
|
|
262
|
-
return abs !== null && existsSync(abs);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// isAllowlisted returns true if the given line matches any pattern in the
|
|
266
|
-
// skill's INTENTIONAL_DIFFS entry.
|
|
267
|
-
function isAllowlisted(line: string, name: SkillName): boolean {
|
|
268
|
-
const patterns = INTENTIONAL_DIFFS[name] ?? [];
|
|
269
|
-
return patterns.some((re) => re.test(line));
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// diffLines computes the symmetric difference between two ordered line arrays:
|
|
273
|
-
// lines that are in `aLines` but not `bLines` (bundled-only), and lines that
|
|
274
|
-
// are in `bLines` but not `aLines` (upstream-only). Returns the two sets.
|
|
275
|
-
// This is intentionally set-based (not position-sensitive) to avoid false
|
|
276
|
-
// positives from harmless reorderings of identical content.
|
|
277
|
-
function diffLines(
|
|
278
|
-
aLines: string[],
|
|
279
|
-
bLines: string[],
|
|
280
|
-
): { bundledOnly: string[]; upstreamOnly: string[] } {
|
|
281
|
-
const aCount = new Map<string, number>();
|
|
282
|
-
const bCount = new Map<string, number>();
|
|
283
|
-
for (const l of aLines) aCount.set(l, (aCount.get(l) ?? 0) + 1);
|
|
284
|
-
for (const l of bLines) bCount.set(l, (bCount.get(l) ?? 0) + 1);
|
|
285
|
-
|
|
286
|
-
const bundledOnly: string[] = [];
|
|
287
|
-
const upstreamOnly: string[] = [];
|
|
288
|
-
|
|
289
|
-
for (const [l, cnt] of aCount) {
|
|
290
|
-
const excess = cnt - (bCount.get(l) ?? 0);
|
|
291
|
-
for (let i = 0; i < excess; i++) bundledOnly.push(l);
|
|
292
|
-
}
|
|
293
|
-
for (const [l, cnt] of bCount) {
|
|
294
|
-
const excess = cnt - (aCount.get(l) ?? 0);
|
|
295
|
-
for (let i = 0; i < excess; i++) upstreamOnly.push(l);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return { bundledOnly, upstreamOnly };
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// ── Test suites ───────────────────────────────────────────────────────────────
|
|
302
|
-
|
|
303
|
-
describe('bundled skills', () => {
|
|
304
|
-
describe('pinned-hash snapshot tests', () => {
|
|
305
|
-
for (const name of SKILLS) {
|
|
306
|
-
it(`${name} bundled copy matches pinned hash`, () => {
|
|
307
|
-
const content = readBundled(name);
|
|
308
|
-
const hash = computeHash(content);
|
|
309
|
-
expect(hash).toBe(PINNED_HASHES[name]);
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
describe('skill inventory invariants', () => {
|
|
315
|
-
it('covers every bundled skill directory', () => {
|
|
316
|
-
// Sentinel: if a new skill is added to awa-bundled/skills/ but not
|
|
317
|
-
// PINNED_HASHES, this test fails — forcing the author to register it.
|
|
318
|
-
const skillsDir = join(__dirname, 'skills');
|
|
319
|
-
const entries = readdirSync(skillsDir)
|
|
320
|
-
.filter((name) => statSync(join(skillsDir, name)).isDirectory())
|
|
321
|
-
.sort();
|
|
322
|
-
const registered = [...SKILLS].sort();
|
|
323
|
-
expect(entries).toEqual(registered);
|
|
324
|
-
});
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
// ── Namespace-normalized drift comparison ──────────────────────────────────
|
|
328
|
-
//
|
|
329
|
-
// Each test below compares a bundled SKILL.md against its upstream
|
|
330
|
-
// counterpart after normalization (namespace prefixes stripped) and
|
|
331
|
-
// allowlisting (known intentional divergences removed).
|
|
332
|
-
//
|
|
333
|
-
// The test is skipped — NOT failed — when awa-private is not co-located
|
|
334
|
-
// (e.g. standalone CI clone). The pinned-hash tests above still guard
|
|
335
|
-
// against local-only edits. These tests guard against the cross-repo case:
|
|
336
|
-
// a change landing in one mirror without being back-ported to the other.
|
|
337
|
-
//
|
|
338
|
-
// Workflow when a test fails here:
|
|
339
|
-
// 1. Is the diff intentional? Add a justified entry to INTENTIONAL_DIFFS.
|
|
340
|
-
// 2. Is it real drift? Land the back-port and re-run. Then bump the hash.
|
|
341
|
-
// 3. Is it unclassifiable? Surface it as unclassified drift in the PR body.
|
|
342
|
-
describe('namespace-normalized drift comparison (skipped if awa-private not co-located)', () => {
|
|
343
|
-
// Invariant: for every mirrorable skill, after normalize() and after
|
|
344
|
-
// removing allowlisted lines, the bundled and upstream copies must be
|
|
345
|
-
// line-for-line identical. Any remaining difference is a back-port gap.
|
|
346
|
-
|
|
347
|
-
const mirrorableSkills = SKILLS.filter(
|
|
348
|
-
(s) => s !== 'intent-lock' && s !== 'simplify' && s !== 'refactor',
|
|
349
|
-
);
|
|
350
|
-
|
|
351
|
-
for (const name of mirrorableSkills) {
|
|
352
|
-
it.skipIf(!upstreamAvailable(name))(
|
|
353
|
-
`${name}: normalized bundled matches normalized upstream (after allowlist)`,
|
|
354
|
-
() => {
|
|
355
|
-
// Contract: upstreamAbsPath is non-null when upstreamAvailable() is true.
|
|
356
|
-
const abs = upstreamAbsPath(name) as string;
|
|
357
|
-
const bundledRaw = readBundled(name);
|
|
358
|
-
const upstreamRaw = readFileSync(abs, 'utf8');
|
|
359
|
-
|
|
360
|
-
const bundledLines = normalize(bundledRaw).split('\n');
|
|
361
|
-
const upstreamLines = normalize(upstreamRaw).split('\n');
|
|
362
|
-
|
|
363
|
-
// Compute symmetric difference: lines unique to each side.
|
|
364
|
-
// Context lines (identical on both sides) are ignored — we only care
|
|
365
|
-
// about lines that changed.
|
|
366
|
-
const { bundledOnly, upstreamOnly } = diffLines(
|
|
367
|
-
bundledLines,
|
|
368
|
-
upstreamLines,
|
|
369
|
-
);
|
|
370
|
-
|
|
371
|
-
// Remove allowlisted divergences from each side.
|
|
372
|
-
const unexpectedBundledOnly = bundledOnly.filter(
|
|
373
|
-
(l) => !isAllowlisted(l, name),
|
|
374
|
-
);
|
|
375
|
-
const unexpectedUpstreamOnly = upstreamOnly.filter(
|
|
376
|
-
(l) => !isAllowlisted(l, name),
|
|
377
|
-
);
|
|
378
|
-
|
|
379
|
-
if (
|
|
380
|
-
unexpectedBundledOnly.length > 0 ||
|
|
381
|
-
unexpectedUpstreamOnly.length > 0
|
|
382
|
-
) {
|
|
383
|
-
const lines: string[] = [
|
|
384
|
-
`--- bundled (normalized, non-allowlisted unique lines)`,
|
|
385
|
-
`+++ upstream (normalized, non-allowlisted unique lines)`,
|
|
386
|
-
];
|
|
387
|
-
for (const l of unexpectedBundledOnly) lines.push(`-${l}`);
|
|
388
|
-
for (const l of unexpectedUpstreamOnly) lines.push(`+${l}`);
|
|
389
|
-
|
|
390
|
-
throw new Error(
|
|
391
|
-
`Namespace-normalized drift detected in ${name}.\n` +
|
|
392
|
-
` Bundled: ${bundledPath(name)}\n` +
|
|
393
|
-
` Upstream: ${abs}\n` +
|
|
394
|
-
` If the diff is intentional, add a justified entry to INTENTIONAL_DIFFS['${name}'].\n` +
|
|
395
|
-
` If it is real drift, back-port the change and bump the pinned hash.\n\n` +
|
|
396
|
-
lines.join('\n'),
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
},
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
});
|
|
403
|
-
});
|