@aiwg/cli 2026.10.10 → 2026.10.12

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 (156) hide show
  1. package/agentic/code/providers/model-capabilities.v1.json +78 -14
  2. package/agentic/code/providers/model-catalog.v1.json +62 -12
  3. package/dist/src/artifacts/binding-target-identity.js +51 -0
  4. package/dist/src/artifacts/checksum-manifest.js +12 -0
  5. package/dist/src/artifacts/index-builder.js +11 -13
  6. package/dist/src/artifacts/query-engine.js +5 -11
  7. package/dist/src/cli/handlers/doctor-help.js +32 -0
  8. package/dist/src/cli/handlers/gates.js +19 -18
  9. package/dist/src/cli/handlers/models-help.js +7 -1
  10. package/dist/src/cli/handlers/models.js +3 -3
  11. package/dist/src/cli/handlers/script-runner.js +8 -5
  12. package/dist/src/cli/handlers/sessions-intake.js +104 -0
  13. package/dist/src/cli/handlers/sessions-tail.js +172 -0
  14. package/dist/src/cli/handlers/sessions.js +287 -31
  15. package/dist/src/cli/handlers/skill-lint.js +10 -3
  16. package/dist/src/cli/handlers/steward-format.js +100 -0
  17. package/dist/src/cli/handlers/steward-models.js +167 -0
  18. package/dist/src/cli/handlers/steward.js +14 -248
  19. package/dist/src/cli/handlers/use-bundle-counts.js +61 -0
  20. package/dist/src/cli/handlers/use.js +18 -28
  21. package/dist/src/cli/handlers/utilities.js +1 -24
  22. package/dist/src/config/aiwg-config.js +7 -0
  23. package/dist/src/config/authorization-cli.js +89 -0
  24. package/dist/src/config/cli-help.js +1 -0
  25. package/dist/src/config/cli.js +10 -1
  26. package/dist/src/config/worker-context.js +337 -0
  27. package/dist/src/decision/context-pruning.js +3 -2
  28. package/dist/src/decision/ensemble/contract.js +15 -4
  29. package/dist/src/decision/ensemble-study/artifacts.js +5 -0
  30. package/dist/src/decision/ensemble-study/score.js +4 -3
  31. package/dist/src/decision/feature-export.js +3 -9
  32. package/dist/src/decision/heldout/collector.js +2 -2
  33. package/dist/src/decision/issue-triage/runtime.js +3 -9
  34. package/dist/src/decision/qualification/gates.js +2 -1
  35. package/dist/src/decision/qualification/release.js +2 -3
  36. package/dist/src/decision/routing/report.js +2 -0
  37. package/dist/src/decision/sdlc-screening.js +3 -5
  38. package/dist/src/decision/sensitivity/comparative.js +5 -3
  39. package/dist/src/gates/driver.js +6 -77
  40. package/dist/src/gates/evaluate.js +4 -11
  41. package/dist/src/gates/floors.js +4 -4
  42. package/dist/src/gates/index.js +3 -3
  43. package/dist/src/gates/lattice.js +26 -0
  44. package/dist/src/gates/registry.js +2 -1
  45. package/dist/src/gates/report.js +19 -7
  46. package/dist/src/issues/audit-cli.js +120 -0
  47. package/dist/src/issues/audit-contract.js +64 -0
  48. package/dist/src/issues/audit-local.js +82 -0
  49. package/dist/src/issues/audit.js +220 -0
  50. package/dist/src/lint/rule-activation.js +15 -1
  51. package/dist/src/mcp/tools/worker-invoke.mjs +108 -10
  52. package/dist/src/mission-protocol/canonical-codecs.js +270 -0
  53. package/dist/src/mission-protocol/codecs.js +22 -550
  54. package/dist/src/mission-protocol/health-trace-codecs.js +281 -0
  55. package/dist/src/mission-protocol/health-trace-types.js +43 -0
  56. package/dist/src/mission-protocol/types.js +2 -42
  57. package/dist/src/mission-protocol/worker-contract-json.js +10 -0
  58. package/dist/src/mission-protocol/worker-review-gate.js +100 -0
  59. package/dist/src/mission-protocol/worker-review-types.js +2 -0
  60. package/dist/src/models/catalog-policy.js +280 -0
  61. package/dist/src/models/index.js +1 -1
  62. package/dist/src/models/model-capabilities.v1.json +78 -14
  63. package/dist/src/models/model-catalog.v1.json +62 -12
  64. package/dist/src/models/policy-resources.js +28 -0
  65. package/dist/src/models/provider-policy.js +10 -539
  66. package/dist/src/models/resolver.js +26 -8
  67. package/dist/src/models/router.js +15 -2
  68. package/dist/src/models/tiers.js +13 -0
  69. package/dist/src/models/types.js +0 -6
  70. package/dist/src/models/worker-ceilings.js +59 -0
  71. package/dist/src/models/worker-invocation-policy.js +243 -0
  72. package/dist/src/models/worker-plan.js +16 -20
  73. package/dist/src/models/worker-review-lineage.js +55 -0
  74. package/dist/src/models/worker-selection-policy.js +1 -1
  75. package/dist/src/models/wrapper-route.js +6 -3
  76. package/dist/src/policy/authorization.js +30 -1
  77. package/dist/src/policy/hitl-config.js +63 -0
  78. package/dist/src/policy/hitl-receipt.js +57 -0
  79. package/dist/src/policy/hitl-types.js +4 -0
  80. package/dist/src/policy/hitl-validation.js +39 -0
  81. package/dist/src/policy/hitl.js +117 -0
  82. package/dist/src/process/aiwg-probe-command.d.mts +15 -0
  83. package/dist/src/process/aiwg-probe-command.mjs +37 -0
  84. package/dist/src/rules/activation.d.mts +1 -1
  85. package/dist/src/rules/activation.mjs +0 -2
  86. package/dist/src/rules/gate-coverage.js +51 -0
  87. package/dist/src/rules/gate-references.js +16 -0
  88. package/dist/src/rules/rule-frontmatter.schema.json +7 -1
  89. package/dist/src/serve/worker-command-request.js +21 -4
  90. package/dist/src/serve/worker-invocation-preparation.js +3 -1
  91. package/dist/src/serve/worker-invocation.js +21 -10
  92. package/dist/src/sessions/adapters/deepseek-harness.js +1 -0
  93. package/dist/src/sessions/adapters/grok-build-native.js +269 -0
  94. package/dist/src/sessions/adapters/grok-build.js +57 -15
  95. package/dist/src/sessions/adapters/grokbot.js +7 -0
  96. package/dist/src/sessions/adapters/hermes-state-db.js +252 -0
  97. package/dist/src/sessions/adapters/hermes.js +42 -3
  98. package/dist/src/sessions/adapters/warp.js +1 -1
  99. package/dist/src/sessions/batch-import.js +44 -6
  100. package/dist/src/sessions/contracts.js +20 -1
  101. package/dist/src/sessions/coverage-preflight.js +7 -1
  102. package/dist/src/sessions/discovery-capability.js +38 -0
  103. package/dist/src/sessions/fortemi-export-mapping.js +33 -3
  104. package/dist/src/sessions/host-catalog.js +157 -0
  105. package/dist/src/sessions/image-blobs.js +141 -0
  106. package/dist/src/sessions/importer.js +9 -232
  107. package/dist/src/sessions/index.js +6 -0
  108. package/dist/src/sessions/intake-dir.js +71 -0
  109. package/dist/src/sessions/normalization.js +251 -0
  110. package/dist/src/sessions/refresh.js +25 -2
  111. package/dist/src/sessions/repository.js +164 -22
  112. package/dist/src/sessions/tail.js +91 -0
  113. package/dist/src/sessions/workspace-discovery.js +88 -9
  114. package/dist/src/skills/agent-skill-sidecar.js +28 -0
  115. package/dist/src/skills/agent-skills.js +4 -23
  116. package/dist/src/skills/claude-surface-projection.js +25 -0
  117. package/dist/src/skills/deployer.js +8 -288
  118. package/dist/src/skills/deployment-policy.js +114 -0
  119. package/dist/src/skills/deployment-projection.js +152 -0
  120. package/dist/src/skills/deployment-types.js +20 -0
  121. package/dist/src/skills/exporter.js +8 -31
  122. package/dist/src/skills/provider-extensions.js +41 -0
  123. package/dist/src/skills/run.js +6 -0
  124. package/dist/src/skills/types.js +0 -8
  125. package/dist/src/skills/validator-resources.js +68 -0
  126. package/dist/src/skills/validator.js +6 -55
  127. package/dist/src/storage/cli-migrate-options.js +125 -0
  128. package/dist/src/storage/cli-migrate.js +111 -0
  129. package/dist/src/storage/cli.js +4 -279
  130. package/dist/src/storage/migration-adapter.js +79 -0
  131. package/dist/src/storage/migration-copy.js +108 -0
  132. package/dist/src/storage/migration-protocol.js +14 -197
  133. package/dist/src/storage/migration-records.js +186 -0
  134. package/dist/src/storage/migration-types.js +12 -0
  135. package/dist/src/tracker/forge-http.js +2 -1
  136. package/dist/src/tracker/issue-pages.js +110 -0
  137. package/package.json +7 -1
  138. package/schemas/mission-v1.schema.json +335 -0
  139. package/schemas/models/model-catalog.v1.schema.json +19 -1
  140. package/schemas/models/model-config-compatibility.v1.schema.json +3 -2
  141. package/schemas/models/model-policy.v1.schema.json +1 -1
  142. package/tools/agents/providers/provider-paths-staging.mjs +35 -0
  143. package/tools/agents/providers/rule-binding-targets.mjs +146 -0
  144. package/tools/agents/providers/rule-delivery.mjs +7 -126
  145. package/tools/ralph-external/lib/muse-adapter.mjs +10 -0
  146. package/tools/ralph-external/lib/worker-context.mjs +237 -0
  147. package/tools/ralph-external/lib/worker-invocation-ancestry.mjs +132 -0
  148. package/tools/ralph-external/lib/worker-invocation-processes.mjs +123 -0
  149. package/tools/ralph-external/lib/worker-isolation-status.mjs +48 -0
  150. package/tools/ralph-external/lib/worker-isolation.mjs +17 -29
  151. package/tools/ralph-external/lib/worker-launch-cli.mjs +3 -1
  152. package/tools/ralph-external/lib/worker-overflow.mjs +48 -13
  153. package/tools/ralph-external/lib/worker-retry-evidence.mjs +103 -0
  154. package/tools/ralph-external/lib/worker-termination.mjs +82 -0
  155. package/tools/ralph-external/session-launcher.mjs +51 -96
  156. package/vscode-extension/schemas/aiwg.config.v1.json +177 -0
@@ -0,0 +1,120 @@
1
+ // Wire issue-audit into the existing skill executor and configured tracker/label routing.
2
+ import { parseArgs } from 'node:util';
3
+ import { resolve } from 'node:path';
4
+ import { resolveIssueLabels } from '../config/aiwg-config.js';
5
+ import { projectAiwgWritePath } from '../config/project-artifacts.js';
6
+ import { resolveTrackerAuthority, chooseTrackerAccess } from '../tracker/capability-protocol.js';
7
+ import { readConfig, readGitRemoteUrls } from '../tracker/project-inputs.js';
8
+ import { cliForgeTransport, httpForgeTransport, forgeEndpointFromRemote } from '../tracker/forge-http.js';
9
+ import { createForgeAuditTracker } from '../tracker/issue-pages.js';
10
+ import { resolveToken } from './live.js';
11
+ import { localIssueRoot } from './local.js';
12
+ import { createLocalAuditTracker } from './audit-local.js';
13
+ import { runIssueAudit, renderIssueAudit, needsAuditContent } from './audit.js';
14
+ import { AuditStop, auditDigest, parseAuditFilter } from './audit-contract.js';
15
+ export async function main(args, cwd = process.cwd(), dependencies = {}) {
16
+ const output = dependencies.output ?? console.log;
17
+ let format = 'markdown';
18
+ try {
19
+ const { values: v } = parseArgs({ args, strict: true, options: {
20
+ filter: { type: 'string' }, guidance: { type: 'string' }, provider: { type: 'string' },
21
+ checkpoint: { type: 'string' }, 'page-size': { type: 'string' }, 'max-items': { type: 'string' }, 'max-pages': { type: 'string' },
22
+ 'max-bytes': { type: 'string' }, 'max-context-bytes': { type: 'string' },
23
+ select: { type: 'string' }, format: { type: 'string' }, 'all-open': { type: 'boolean' },
24
+ 'dry-run': { type: 'boolean' }, apply: { type: 'boolean' }, interactive: { type: 'boolean' }, help: { type: 'boolean' },
25
+ } });
26
+ format = v.format ?? 'markdown';
27
+ if (!['json', 'markdown'].includes(format))
28
+ throw new AuditStop('format-invalid');
29
+ if (v.help) {
30
+ output('aiwg run skill issue-audit -- [--filter "status:open label:bug"] [--guidance TEXT] [--provider local|gitea|github]\n'
31
+ + ' [--checkpoint PATH] [--page-size 25] [--max-items 250] [--max-pages 100] [--max-bytes 65536]\n'
32
+ + ' [--max-context-bytes 262144] [--select ID,ID] [--format json|markdown]\n'
33
+ + 'Rerun the same scope/checkpoint to resume. Execution is read-only; cleanup decisions use the instructional workflow.');
34
+ return 0;
35
+ }
36
+ if (v.apply || v.interactive)
37
+ throw new AuditStop('use-instructional-workflow-for-interactive-or-apply');
38
+ const number = (name, fallback) => {
39
+ const value = Number(v[name] ?? fallback);
40
+ if (!Number.isSafeInteger(value) || value < 1)
41
+ throw new AuditStop(`invalid-${name}`);
42
+ return value;
43
+ };
44
+ const pageSize = number('page-size', 25);
45
+ const maxItems = number('max-items', 250);
46
+ const maxPages = number('max-pages', 100);
47
+ const maxBytes = number('max-bytes', 65_536);
48
+ const maxContextBytes = number('max-context-bytes', 262_144);
49
+ if (pageSize > 100)
50
+ throw new AuditStop('invalid-page-size');
51
+ const config = dependencies.config === undefined ? await readConfig(cwd) : dependencies.config;
52
+ const authority = resolveTrackerAuthority(config, dependencies.remoteUrls ?? await readGitRemoteUrls(cwd));
53
+ const provider = v.provider ?? authority.provider;
54
+ if (provider !== 'local' && provider !== 'gitea' && provider !== 'github')
55
+ throw new AuditStop('tracker-provider-unresolved');
56
+ const taxonomy = resolveIssueLabels(config?.issues, provider);
57
+ if (taxonomy.diagnostics.some(d => d.severity === 'error'))
58
+ throw new AuditStop('label-taxonomy-invalid');
59
+ const filter = v.filter ?? 'status:open';
60
+ const parsedFilter = parseAuditFilter(filter);
61
+ const nativeFilter = [`status:${parsedFilter.status}`, ...parsedFilter.labels.map(role => `label:${JSON.stringify(taxonomy.labels[role]?.resolved_name ?? role)}`)].join(' ');
62
+ const selection = v.select === undefined ? null : new Set(v.select.split(',').map(s => s.trim()).filter(Boolean));
63
+ const checkpoint = v.checkpoint ? resolve(cwd, v.checkpoint) : projectAiwgWritePath(cwd, 'working', 'issue-audit', 'checkpoint.json');
64
+ const env = dependencies.env ?? process.env;
65
+ let tracker = dependencies.tracker;
66
+ if (!tracker) {
67
+ if (provider === 'local')
68
+ tracker = createLocalAuditTracker(localIssueRoot(cwd));
69
+ else {
70
+ const endpoint = forgeEndpointFromRemote(provider, authority.issueTrackerUrl);
71
+ if (!endpoint)
72
+ throw new AuditStop('tracker-repository-unresolved');
73
+ const token = resolveToken(provider, env);
74
+ const access = chooseTrackerAccess({ ...authority, provider }, [
75
+ { kind: 'http-api', provider, remoteName: authority.issueTrackerRemote, available: !!token, authenticated: !!token },
76
+ { kind: 'cli', provider, remoteName: authority.issueTrackerRemote, available: provider === 'github', cli: 'gh' },
77
+ ]);
78
+ const transport = access.kind === 'http-api' && token
79
+ ? httpForgeTransport({ provider, apiBaseUrl: endpoint.apiBaseUrl, token, maxResponseBytes: maxBytes })
80
+ : access.kind === 'cli' ? cliForgeTransport({ provider, hostname: endpoint.host, maxResponseBytes: maxBytes }) : null;
81
+ if (!transport)
82
+ throw new AuditStop('tracker-no-access-route');
83
+ tracker = createForgeAuditTracker(provider, endpoint.repository, transport, `${provider}:${endpoint.apiBaseUrl}:${endpoint.repository}:${config?.remotes?.tracker_actor?.login ?? ''}`);
84
+ }
85
+ }
86
+ const nativeTracker = tracker;
87
+ const semanticTracker = { ...nativeTracker,
88
+ async listMetadata(options) {
89
+ const page = await nativeTracker.listMetadata(options);
90
+ return { ...page, items: page.items.map(i => ({ ...i, labels: [...i.labels,
91
+ ...Object.values(taxonomy.labels).filter(l => i.labels.includes(l.resolved_name)).map(l => l.role)] })) };
92
+ },
93
+ };
94
+ const controller = new AbortController();
95
+ const interrupt = () => controller.abort();
96
+ process.once('SIGINT', interrupt);
97
+ process.once('SIGTERM', interrupt);
98
+ try {
99
+ const report = await runIssueAudit(semanticTracker, { checkpoint, pageSize, maxItems, maxPages, maxBytes, maxContextBytes,
100
+ filter: nativeFilter, guidance: v.guidance, signal: controller.signal,
101
+ policyKey: auditDigest([selection ? [...selection].sort() : null, taxonomy.labels]),
102
+ select: i => selection ? selection.has(i.id) : needsAuditContent(i) });
103
+ output(format === 'json' ? JSON.stringify({ ...report, scope: { provider, filter }, diagnostics: taxonomy.diagnostics })
104
+ : `${renderIssueAudit(report)}${taxonomy.diagnostics.map(d => `\n${d.severity}: ${d.message}`).join('')}`);
105
+ return report.coverage.incomplete ? 2 : 0;
106
+ }
107
+ finally {
108
+ process.removeListener('SIGINT', interrupt);
109
+ process.removeListener('SIGTERM', interrupt);
110
+ }
111
+ }
112
+ catch (error) {
113
+ const reason = error instanceof AuditStop ? error.reason : 'audit-error';
114
+ output(format === 'json' ? JSON.stringify({ mode: 'read-only', coverage: {
115
+ totalSeen: 0, totalHydrated: 0, totalCompleted: 0, incomplete: true, reason
116
+ } }) : `# Issue Audit\n\nCoverage: INCOMPLETE (${reason})`);
117
+ return 2;
118
+ }
119
+ }
120
+ //# sourceMappingURL=audit-cli.js.map
@@ -0,0 +1,64 @@
1
+ // Shared bounded retrieval and checkpoint envelope for issue-audit backends.
2
+ import { createHash } from 'node:crypto';
3
+ export class AuditStop extends Error {
4
+ reason;
5
+ constructor(reason) {
6
+ super(reason);
7
+ this.reason = reason;
8
+ }
9
+ }
10
+ export const auditBytes = (value) => Buffer.byteLength(JSON.stringify(value));
11
+ export const auditDigest = (value) => createHash('sha256').update(JSON.stringify(value)).digest('hex');
12
+ export function boundedPageSize(value) {
13
+ if (!Number.isSafeInteger(value) || value < 1 || value > 100)
14
+ throw new AuditStop('invalid-page-size');
15
+ return value;
16
+ }
17
+ export function encodeAuditCursor(value) {
18
+ return Buffer.from(JSON.stringify(value)).toString('base64url');
19
+ }
20
+ export function decodeAuditCursor(cursor) {
21
+ try {
22
+ if (!/^[A-Za-z0-9_-]{1,8192}$/.test(cursor))
23
+ throw new Error();
24
+ return JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
25
+ }
26
+ catch {
27
+ throw new AuditStop('cursor-invalidated');
28
+ }
29
+ }
30
+ /** Reject unsupported filters rather than quietly broadening audit coverage. */
31
+ export function parseAuditFilter(expression) {
32
+ const result = { status: 'open', labels: [] };
33
+ const tokens = /(?:^|\s+)(status|label):("(?:[^"\\]|\\.)*"|'[^']*'|[^\s"']+)/gy;
34
+ let position = 0;
35
+ const input = expression.trim();
36
+ while (position < input.length) {
37
+ const match = tokens.exec(input);
38
+ if (!match)
39
+ throw new AuditStop('filter-invalid');
40
+ let value = match[2];
41
+ if (value.startsWith('"')) {
42
+ try {
43
+ value = JSON.parse(value);
44
+ }
45
+ catch {
46
+ throw new AuditStop('filter-invalid');
47
+ }
48
+ }
49
+ else if (value.startsWith("'"))
50
+ value = value.slice(1, -1);
51
+ if (!value)
52
+ throw new AuditStop('filter-invalid');
53
+ if (match[1] === 'status') {
54
+ if (!['open', 'closed', 'all'].includes(value))
55
+ throw new AuditStop('filter-invalid');
56
+ result.status = value;
57
+ }
58
+ else
59
+ result.labels.push(value);
60
+ position = tokens.lastIndex;
61
+ }
62
+ return result;
63
+ }
64
+ //# sourceMappingURL=audit-contract.js.map
@@ -0,0 +1,82 @@
1
+ // Adapt the existing local issue index and selective issue reads to the audit envelope.
2
+ import { readFile, stat } from 'node:fs/promises';
3
+ import { join, resolve, sep } from 'node:path';
4
+ import { LocalIssueProviderCore } from './local.js';
5
+ import { AuditStop, auditDigest, auditBytes, boundedPageSize, decodeAuditCursor, encodeAuditCursor, parseAuditFilter } from './audit-contract.js';
6
+ export function createLocalAuditTracker(root) {
7
+ const provider = new LocalIssueProviderCore(root);
8
+ const identity = `local:${root}`;
9
+ return {
10
+ identity,
11
+ async listMetadata(options) {
12
+ options.signal?.throwIfAborted();
13
+ const size = boundedPageSize(options.pageSize);
14
+ const filter = parseAuditFilter(options.filter);
15
+ // Hash metadata only. Any changed index invalidates an offset cursor, avoiding silent omissions.
16
+ const fingerprint = auditDigest(await readFile(join(root, 'index/issues.index.json'), 'utf8'));
17
+ const scope = auditDigest([identity, size, filter]);
18
+ const cursor = options.cursor ? decodeAuditCursor(options.cursor) : null;
19
+ if (options.cursor && (!cursor || typeof cursor !== 'object'))
20
+ throw new AuditStop('cursor-invalidated');
21
+ if (cursor && (cursor.scope !== scope || typeof cursor.offset !== 'string' || !/^\d+$/.test(cursor.offset))) {
22
+ throw new AuditStop('cursor-invalidated');
23
+ }
24
+ if (cursor && cursor.fingerprint !== fingerprint)
25
+ throw new AuditStop('tracker-changed');
26
+ const result = await provider.listIssues({ limit: size, cursor: cursor?.offset,
27
+ filter: { status: filter.status === 'all' ? undefined : filter.status, labels: filter.labels } });
28
+ if (auditDigest(await readFile(join(root, 'index/issues.index.json'), 'utf8')) !== fingerprint)
29
+ throw new AuditStop('tracker-changed');
30
+ const items = result.issues.map(i => ({ id: i.id, title: i.title, status: i.status,
31
+ labels: [...i.labels, i.type, i.priority], updatedAt: i.updated_at }));
32
+ const bytes = auditBytes(items);
33
+ if (bytes > options.maxBytes)
34
+ throw new AuditStop('byte-budget');
35
+ return { items, bytes, complete: !result.nextCursor,
36
+ nextCursor: result.nextCursor ? encodeAuditCursor({ scope, fingerprint, offset: result.nextCursor }) : null };
37
+ },
38
+ async hydrate(issue, options) {
39
+ options.signal?.throwIfAborted();
40
+ if (!/^[A-Z][A-Z0-9]*-\d+$/.test(issue.id))
41
+ throw new AuditStop('invalid-issue-id');
42
+ // Preflight the selected files before the existing reader loads any bodies.
43
+ let bytes = 0;
44
+ const account = async (file) => {
45
+ const size = await stat(file).then(s => s.size).catch((error) => {
46
+ if (error.code === 'ENOENT')
47
+ return 0;
48
+ throw error;
49
+ });
50
+ bytes += size;
51
+ if (bytes > options.maxBytes)
52
+ throw new AuditStop('byte-budget');
53
+ };
54
+ await account(join(root, 'items', `${issue.id}.md`));
55
+ const eventsPath = join(root, 'events', `${issue.id}.jsonl`);
56
+ await account(eventsPath);
57
+ const events = await readFile(eventsPath, 'utf8').catch((error) => {
58
+ if (error.code === 'ENOENT')
59
+ return '';
60
+ throw error;
61
+ });
62
+ for (const line of events.split(/\r?\n/).filter(Boolean)) {
63
+ const event = JSON.parse(line);
64
+ if (!event.body_path)
65
+ continue;
66
+ const file = resolve(root, event.body_path);
67
+ if (!file.startsWith(resolve(root) + sep))
68
+ throw new AuditStop('invalid-comment-path');
69
+ await account(file);
70
+ }
71
+ const record = await provider.getIssue(issue.id, { body: true, comments: 'all' });
72
+ if (record.fields.updated_at !== issue.updatedAt)
73
+ throw new AuditStop('tracker-changed');
74
+ const content = { body: record.body, comments: record.events.filter(e => e.type === 'comment').map(e => e.body ?? '') };
75
+ bytes = Math.max(bytes, auditBytes(content));
76
+ if (bytes > options.maxBytes)
77
+ throw new AuditStop('byte-budget');
78
+ return { ...content, bytes };
79
+ },
80
+ };
81
+ }
82
+ //# sourceMappingURL=audit-local.js.map
@@ -0,0 +1,220 @@
1
+ // Execute bounded audit batches with atomic, content-minimized item checkpoints.
2
+ import { readFile } from 'node:fs/promises';
3
+ import { atomicConfigWrite } from '../config/worker-write.js';
4
+ import { acquireDirectoryLock, DirectoryLockContentionError } from '../artifacts/prebuilt-build-lock.js';
5
+ import { ForgeReadError } from '../tracker/forge-http.js';
6
+ import { AuditStop, auditDigest, auditBytes, boundedPageSize, parseAuditFilter, } from './audit-contract.js';
7
+ export const needsAuditContent = (issue) => /\b(epic|deferred|duplicate|superseded|fixed)\b/i.test(`${issue.title} ${issue.labels.join(' ')}`);
8
+ /** Deterministic triage candidates; deeper semantic conclusions belong to the auditor. */
9
+ export function auditFinding(issue, content) {
10
+ const text = `${issue.title} ${issue.labels.join(' ')}`;
11
+ const references = content && /(?:#\d+|[A-Z][A-Z0-9]*-\d+)/.test(`${content.body}\n${content.comments.join('\n')}`);
12
+ if (/\b(blocker|critical|P[01])\b/i.test(text))
13
+ return { id: issue.id, category: 'High Priority', recommendation: 'Review blocker for address-issues' };
14
+ if (/\bepic\b/i.test(text))
15
+ return { id: issue.id, category: 'Epic Hygiene', recommendation: references
16
+ ? 'Check linked child status and acceptance against current issues' : 'Add child issue references before checking epic completion' };
17
+ if (/\b(duplicate|overlap)\b/i.test(text))
18
+ return { id: issue.id, category: 'Duplicate / Relationship Candidates', recommendation: references
19
+ ? 'Verify overlap with referenced issues before recommending cleanup' : 'Identify a related issue before recommending duplicate cleanup' };
20
+ if (/\b(deferred|superseded|fixed)\b/i.test(text))
21
+ return { id: issue.id, category: 'Cleanup Candidates', recommendation: content && !/\b\d{4}-\d{2}-\d{2}\b/.test(`${content.body}\n${content.comments.join('\n')}`)
22
+ ? 'Add a follow-up date and verify the cleanup decision' : 'Verify cleanup decision and follow-up date' };
23
+ return { id: issue.id, category: 'Next Actions', recommendation: content && !content.body.trim()
24
+ ? 'Add issue context and acceptance criteria' : 'Review priority and scope before address-issues' };
25
+ }
26
+ async function loadCheckpoint(file, scope) {
27
+ let raw;
28
+ try {
29
+ raw = await readFile(file, 'utf8');
30
+ }
31
+ catch (error) {
32
+ if (error.code !== 'ENOENT')
33
+ throw error;
34
+ return { version: 1, scope, cursor: null, pending: [], pageComplete: false, finished: false,
35
+ completed: [], findings: [], totalSeen: 0, totalHydrated: 0 };
36
+ }
37
+ let value;
38
+ try {
39
+ value = JSON.parse(raw);
40
+ }
41
+ catch {
42
+ throw new AuditStop('checkpoint-invalid');
43
+ }
44
+ if (!value || value.version !== 1 || value.scope !== scope || !Array.isArray(value.completed)
45
+ || !Array.isArray(value.pending) || !Array.isArray(value.findings)
46
+ || typeof value.finished !== 'boolean' || typeof value.pageComplete !== 'boolean'
47
+ || !(value.cursor === null || typeof value.cursor === 'string')
48
+ || !Number.isSafeInteger(value.totalSeen) || value.totalSeen < 0
49
+ || !Number.isSafeInteger(value.totalHydrated) || value.totalHydrated < 0
50
+ || value.totalHydrated > value.completed.length || value.totalSeen < value.completed.length
51
+ || new Set(value.completed).size !== value.completed.length
52
+ || value.completed.some(id => typeof id !== 'string')
53
+ || value.pending.some(i => !i || typeof i.id !== 'string' || typeof i.title !== 'string'
54
+ || typeof i.status !== 'string' || typeof i.updatedAt !== 'string' || !Array.isArray(i.labels)
55
+ || i.labels.some(l => typeof l !== 'string') || value.completed.includes(i.id))
56
+ || new Set(value.pending.map(i => i.id)).size !== value.pending.length
57
+ || value.totalSeen !== value.completed.length + value.pending.length
58
+ || (value.finished && (value.pending.length > 0 || !value.pageComplete || value.cursor !== null))
59
+ || value.findings.length !== value.completed.length
60
+ || value.findings.some((f, i) => !f || f.id !== value.completed[i] || typeof f.recommendation !== 'string')) {
61
+ throw new AuditStop(value?.scope !== scope ? 'checkpoint-scope-mismatch' : 'checkpoint-invalid');
62
+ }
63
+ return value;
64
+ }
65
+ export async function runIssueAudit(tracker, options) {
66
+ const pageSize = boundedPageSize(options.pageSize ?? 25);
67
+ const maxItems = options.maxItems ?? 250;
68
+ const maxPages = options.maxPages ?? 100;
69
+ const maxBytes = options.maxBytes ?? 65_536;
70
+ const maxContext = options.maxContextBytes ?? 262_144;
71
+ for (const bound of [maxItems, maxPages, maxBytes, maxContext]) {
72
+ if (!Number.isSafeInteger(bound) || bound < 1)
73
+ throw new AuditStop('invalid-budget');
74
+ }
75
+ const filter = options.filter ?? 'status:open';
76
+ const guidance = options.guidance ?? '';
77
+ parseAuditFilter(filter);
78
+ if ((options.select || options.analyze) && !options.policyKey)
79
+ throw new AuditStop('policy-key-required');
80
+ const scope = auditDigest([tracker.identity, filter, guidance, pageSize, options.policyKey ?? 'metadata-triage-v1']);
81
+ let release;
82
+ try {
83
+ release = await acquireDirectoryLock(`${options.checkpoint}.lock`, { timeoutMs: 0, reclaimDeadOwner: true });
84
+ }
85
+ catch (error) {
86
+ if (error instanceof DirectoryLockContentionError)
87
+ throw new AuditStop('checkpoint-busy');
88
+ throw error;
89
+ }
90
+ try {
91
+ return await execute();
92
+ }
93
+ finally {
94
+ await release();
95
+ }
96
+ async function execute() {
97
+ let state = await loadCheckpoint(options.checkpoint, scope);
98
+ let durable = structuredClone(state);
99
+ const save = async () => {
100
+ try {
101
+ await atomicConfigWrite(options.checkpoint, `${JSON.stringify(state)}\n`);
102
+ }
103
+ catch {
104
+ throw new AuditStop('checkpoint-write-error');
105
+ }
106
+ durable = structuredClone(state);
107
+ };
108
+ const completed = new Set(state.completed);
109
+ const batches = [];
110
+ const visited = new Set();
111
+ let contextBytes = 0;
112
+ let processed = 0;
113
+ let reason = null;
114
+ let batch = { items: 0, bytes: 0 };
115
+ await save();
116
+ try {
117
+ while (!state.finished) {
118
+ if (options.signal?.aborted)
119
+ throw new AuditStop('interrupted');
120
+ if (processed >= maxItems)
121
+ throw new AuditStop('item-budget');
122
+ if (!state.pending.length) {
123
+ if (batches.length >= maxPages)
124
+ throw new AuditStop('page-budget');
125
+ if (visited.has(state.cursor))
126
+ throw new AuditStop('cursor-invalidated');
127
+ if (contextBytes >= maxContext)
128
+ throw new AuditStop('context-budget');
129
+ const available = Math.min(maxBytes, maxContext - contextBytes);
130
+ const page = await tracker.listMetadata({ cursor: state.cursor, pageSize, maxBytes: available, filter, signal: options.signal })
131
+ .catch(error => { if (error instanceof AuditStop && error.reason === 'byte-budget' && available < maxBytes)
132
+ throw new AuditStop('context-budget'); throw error; });
133
+ if (page.items.length > pageSize || page.bytes > maxBytes)
134
+ throw new AuditStop('byte-budget');
135
+ if (page.bytes > maxContext - contextBytes)
136
+ throw new AuditStop('context-budget');
137
+ if (page.nextCursor !== null && page.nextCursor === state.cursor)
138
+ throw new AuditStop('cursor-invalidated');
139
+ if (!page.complete && page.nextCursor === null)
140
+ throw new AuditStop('pagination-incomplete');
141
+ const unique = new Map(page.items.filter(i => !completed.has(i.id)).map(i => [i.id, i]));
142
+ visited.add(state.cursor);
143
+ state.pending = [...unique.values()];
144
+ state.totalSeen += state.pending.length;
145
+ state.cursor = page.nextCursor;
146
+ state.pageComplete = page.complete;
147
+ batch = { items: 0, bytes: page.bytes };
148
+ batches.push(batch);
149
+ contextBytes += page.bytes;
150
+ state.finished = !state.pending.length && state.pageComplete;
151
+ await save();
152
+ if (!state.pending.length)
153
+ continue;
154
+ }
155
+ else if (!batches.length) {
156
+ batch = { items: 0, bytes: auditBytes(state.pending) };
157
+ if (batch.bytes > maxBytes)
158
+ throw new AuditStop('byte-budget');
159
+ contextBytes += batch.bytes;
160
+ batches.push(batch);
161
+ }
162
+ const issue = state.pending[0];
163
+ const metadataBytes = auditBytes(issue);
164
+ if (contextBytes + metadataBytes > maxContext)
165
+ throw new AuditStop('context-budget');
166
+ let content;
167
+ if ((options.select ?? needsAuditContent)(issue)) {
168
+ const remaining = Math.min(maxBytes - batch.bytes, maxContext - contextBytes - metadataBytes);
169
+ if (remaining < 1)
170
+ throw new AuditStop(batch.bytes >= maxBytes ? 'byte-budget' : 'context-budget');
171
+ content = await tracker.hydrate(issue, { maxBytes: remaining, signal: options.signal })
172
+ .catch(error => { if (error instanceof AuditStop && error.reason === 'byte-budget' && remaining < maxBytes - batch.bytes)
173
+ throw new AuditStop('context-budget'); throw error; });
174
+ if (content.bytes > remaining)
175
+ throw new AuditStop('byte-budget');
176
+ }
177
+ const finding = (options.analyze ?? auditFinding)(issue, content);
178
+ if (finding.id !== issue.id)
179
+ throw new AuditStop('finding-id-mismatch');
180
+ state.completed.push(issue.id);
181
+ completed.add(issue.id);
182
+ state.findings.push(finding);
183
+ state.totalHydrated += Number(!!content);
184
+ state.pending.shift();
185
+ state.finished = !state.pending.length && state.pageComplete;
186
+ batch.items++;
187
+ batch.bytes += content?.bytes ?? 0;
188
+ contextBytes += metadataBytes + (content?.bytes ?? 0);
189
+ processed++;
190
+ await save();
191
+ options.afterItem?.(issue.id);
192
+ }
193
+ }
194
+ catch (error) {
195
+ state = durable;
196
+ reason = options.signal?.aborted ? 'interrupted' : error instanceof AuditStop ? error.reason
197
+ : error instanceof ForgeReadError ? (error.code === 'oversized' ? 'byte-budget' : `tracker-${error.code}`) : 'tracker-error';
198
+ }
199
+ return { mode: 'read-only', guidance,
200
+ coverage: { totalSeen: state.totalSeen, totalHydrated: state.totalHydrated, totalCompleted: state.completed.length,
201
+ incomplete: reason !== null || !state.finished, reason },
202
+ continuation: { checkpoint: options.checkpoint, cursor: state.cursor, pending: state.pending.length },
203
+ batches, findings: state.findings };
204
+ }
205
+ }
206
+ export function renderIssueAudit(report) {
207
+ const { coverage: c } = report;
208
+ const lines = ['# Issue Audit', '', 'Mode: read-only', `Guidance: ${report.guidance || 'none'}`, '',
209
+ `Coverage: ${c.incomplete ? `INCOMPLETE (${c.reason ?? 'unfinished'})` : 'complete'}`,
210
+ `Total seen: ${c.totalSeen}; hydrated: ${c.totalHydrated}; completed: ${c.totalCompleted}`, '', '## Findings'];
211
+ for (const category of ['High Priority', 'Cleanup Candidates', 'Epic Hygiene', 'Duplicate / Relationship Candidates', 'Next Actions']) {
212
+ lines.push('', `### ${category}`);
213
+ for (const finding of report.findings.filter(f => f.category === category))
214
+ lines.push(`- ${finding.id} — ${finding.recommendation}`);
215
+ }
216
+ if (c.incomplete)
217
+ lines.push('', `Resume with checkpoint: ${report.continuation.checkpoint}`);
218
+ return `${lines.join('\n')}\n`;
219
+ }
220
+ //# sourceMappingURL=audit.js.map
@@ -6,12 +6,14 @@ import { resolveRuleBindingTarget } from '../artifacts/query-engine.js';
6
6
  import { collectRuleBindingEntries } from '../artifacts/index-builder.js';
7
7
  import { resolveFrameworkIndexRoot } from '../artifacts/framework-index-paths.js';
8
8
  import { parseRuleFrontmatter, RULE_BINDING_TYPES, coreBodyPath, validateCoreBody } from '../rules/activation.mjs';
9
+ import { buildGatesRegistry } from '../gates/driver.js';
10
+ import { ruleGateReferenceErrors } from '../rules/gate-references.js';
9
11
  export const ruleActivationRuleset = {
10
12
  id: 'rule-activation', name: 'Rule activation', framework: 'core', version: '1.0.0',
11
13
  description: 'Validate rule and behavior activation contracts and exact source bindings',
12
14
  rules: [{
13
15
  id: 'rules/activation', name: 'Rule activation contract', severity: 'error',
14
- description: 'Validate activation, summaries, paths, bindings, and migration fallback',
16
+ description: 'Validate activation, summaries, paths, bindings, gate references, and migration fallback',
15
17
  appliesTo: { glob: '**/{rules/**/*.md,behaviors/**/BEHAVIOR.md}' },
16
18
  checks: [{ type: 'rule-activation' }],
17
19
  }],
@@ -38,6 +40,18 @@ export function lintRuleActivation(content, file, projectRoot, entries = [], abs
38
40
  }));
39
41
  if (!result.rule)
40
42
  return diagnostics;
43
+ if (result.rule.enforcedBy?.length) {
44
+ let errors;
45
+ try {
46
+ const { registry } = buildGatesRegistry({ cwd: projectRoot, aiwgRoot: resolveFrameworkIndexRoot(projectRoot) });
47
+ errors = ruleGateReferenceErrors(result.rule.enforcedBy, registry);
48
+ }
49
+ catch (error) {
50
+ errors = [`enforcedBy registry: ${error instanceof Error ? error.message : String(error)}`];
51
+ }
52
+ for (const message of errors)
53
+ diagnostics.push({ ruleId: 'rules/activation', ruleName: 'Rule activation contract', file, severity: 'error', message });
54
+ }
41
55
  if (result.rule.activation === 'core') {
42
56
  const body = coreBodyPath(absolutePath ?? (path.isAbsolute(file) ? file : path.resolve(projectRoot, file)));
43
57
  const problems = fs.existsSync(body)