@aiwg/cli 2026.10.11 → 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 (132) hide show
  1. package/agentic/code/providers/model-catalog.v1.json +62 -12
  2. package/dist/src/artifacts/binding-target-identity.js +51 -0
  3. package/dist/src/artifacts/checksum-manifest.js +12 -0
  4. package/dist/src/artifacts/index-builder.js +11 -13
  5. package/dist/src/artifacts/query-engine.js +5 -11
  6. package/dist/src/cli/handlers/doctor-help.js +32 -0
  7. package/dist/src/cli/handlers/gates.js +19 -18
  8. package/dist/src/cli/handlers/models-help.js +7 -1
  9. package/dist/src/cli/handlers/models.js +3 -3
  10. package/dist/src/cli/handlers/script-runner.js +8 -5
  11. package/dist/src/cli/handlers/sessions-intake.js +104 -0
  12. package/dist/src/cli/handlers/sessions-tail.js +172 -0
  13. package/dist/src/cli/handlers/sessions.js +90 -17
  14. package/dist/src/cli/handlers/skill-lint.js +10 -3
  15. package/dist/src/cli/handlers/steward-format.js +100 -0
  16. package/dist/src/cli/handlers/steward-models.js +167 -0
  17. package/dist/src/cli/handlers/steward.js +14 -248
  18. package/dist/src/cli/handlers/use-bundle-counts.js +61 -0
  19. package/dist/src/cli/handlers/use.js +6 -24
  20. package/dist/src/cli/handlers/utilities.js +1 -24
  21. package/dist/src/config/aiwg-config.js +7 -0
  22. package/dist/src/config/authorization-cli.js +89 -0
  23. package/dist/src/config/cli-help.js +1 -0
  24. package/dist/src/config/cli.js +10 -1
  25. package/dist/src/decision/context-pruning.js +3 -2
  26. package/dist/src/decision/ensemble/contract.js +15 -4
  27. package/dist/src/decision/ensemble-study/artifacts.js +5 -0
  28. package/dist/src/decision/ensemble-study/score.js +4 -3
  29. package/dist/src/decision/feature-export.js +3 -9
  30. package/dist/src/decision/heldout/collector.js +2 -2
  31. package/dist/src/decision/issue-triage/runtime.js +3 -9
  32. package/dist/src/decision/qualification/gates.js +2 -1
  33. package/dist/src/decision/qualification/release.js +2 -3
  34. package/dist/src/decision/routing/report.js +2 -0
  35. package/dist/src/decision/sdlc-screening.js +3 -5
  36. package/dist/src/decision/sensitivity/comparative.js +5 -3
  37. package/dist/src/gates/driver.js +6 -77
  38. package/dist/src/gates/evaluate.js +4 -11
  39. package/dist/src/gates/floors.js +4 -4
  40. package/dist/src/gates/index.js +3 -3
  41. package/dist/src/gates/lattice.js +26 -0
  42. package/dist/src/gates/registry.js +2 -1
  43. package/dist/src/gates/report.js +19 -7
  44. package/dist/src/issues/audit-cli.js +120 -0
  45. package/dist/src/issues/audit-contract.js +64 -0
  46. package/dist/src/issues/audit-local.js +82 -0
  47. package/dist/src/issues/audit.js +220 -0
  48. package/dist/src/lint/rule-activation.js +15 -1
  49. package/dist/src/mission-protocol/canonical-codecs.js +270 -0
  50. package/dist/src/mission-protocol/codecs.js +22 -550
  51. package/dist/src/mission-protocol/health-trace-codecs.js +281 -0
  52. package/dist/src/mission-protocol/health-trace-types.js +43 -0
  53. package/dist/src/mission-protocol/types.js +2 -42
  54. package/dist/src/mission-protocol/worker-contract-json.js +10 -0
  55. package/dist/src/mission-protocol/worker-review-gate.js +100 -0
  56. package/dist/src/mission-protocol/worker-review-types.js +2 -0
  57. package/dist/src/models/catalog-policy.js +280 -0
  58. package/dist/src/models/index.js +1 -1
  59. package/dist/src/models/model-catalog.v1.json +62 -12
  60. package/dist/src/models/policy-resources.js +28 -0
  61. package/dist/src/models/provider-policy.js +10 -546
  62. package/dist/src/models/resolver.js +26 -8
  63. package/dist/src/models/router.js +15 -2
  64. package/dist/src/models/tiers.js +13 -0
  65. package/dist/src/models/types.js +0 -6
  66. package/dist/src/models/worker-ceilings.js +59 -0
  67. package/dist/src/models/worker-invocation-policy.js +243 -0
  68. package/dist/src/models/worker-plan.js +16 -20
  69. package/dist/src/models/worker-review-lineage.js +55 -0
  70. package/dist/src/models/worker-selection-policy.js +1 -1
  71. package/dist/src/models/wrapper-route.js +6 -3
  72. package/dist/src/policy/authorization.js +1 -0
  73. package/dist/src/policy/hitl-config.js +63 -0
  74. package/dist/src/policy/hitl-receipt.js +57 -0
  75. package/dist/src/policy/hitl-types.js +4 -0
  76. package/dist/src/policy/hitl-validation.js +39 -0
  77. package/dist/src/policy/hitl.js +117 -0
  78. package/dist/src/rules/activation.d.mts +1 -1
  79. package/dist/src/rules/activation.mjs +0 -2
  80. package/dist/src/rules/gate-coverage.js +51 -0
  81. package/dist/src/rules/gate-references.js +16 -0
  82. package/dist/src/rules/rule-frontmatter.schema.json +7 -1
  83. package/dist/src/serve/worker-command-request.js +21 -4
  84. package/dist/src/serve/worker-invocation-preparation.js +3 -1
  85. package/dist/src/serve/worker-invocation.js +21 -10
  86. package/dist/src/sessions/adapters/deepseek-harness.js +1 -0
  87. package/dist/src/sessions/adapters/grok-build-native.js +269 -0
  88. package/dist/src/sessions/adapters/grok-build.js +57 -15
  89. package/dist/src/sessions/adapters/hermes-state-db.js +252 -0
  90. package/dist/src/sessions/adapters/hermes.js +42 -3
  91. package/dist/src/sessions/adapters/warp.js +1 -1
  92. package/dist/src/sessions/batch-import.js +10 -0
  93. package/dist/src/sessions/contracts.js +1 -1
  94. package/dist/src/sessions/discovery-capability.js +38 -0
  95. package/dist/src/sessions/importer.js +8 -251
  96. package/dist/src/sessions/index.js +4 -0
  97. package/dist/src/sessions/intake-dir.js +71 -0
  98. package/dist/src/sessions/normalization.js +251 -0
  99. package/dist/src/sessions/tail.js +91 -0
  100. package/dist/src/sessions/workspace-discovery.js +83 -7
  101. package/dist/src/skills/agent-skill-sidecar.js +28 -0
  102. package/dist/src/skills/agent-skills.js +4 -23
  103. package/dist/src/skills/claude-surface-projection.js +25 -0
  104. package/dist/src/skills/deployer.js +8 -288
  105. package/dist/src/skills/deployment-policy.js +114 -0
  106. package/dist/src/skills/deployment-projection.js +152 -0
  107. package/dist/src/skills/deployment-types.js +20 -0
  108. package/dist/src/skills/exporter.js +8 -31
  109. package/dist/src/skills/provider-extensions.js +41 -0
  110. package/dist/src/skills/run.js +6 -0
  111. package/dist/src/skills/types.js +0 -8
  112. package/dist/src/skills/validator-resources.js +68 -0
  113. package/dist/src/skills/validator.js +6 -55
  114. package/dist/src/storage/cli-migrate-options.js +125 -0
  115. package/dist/src/storage/cli-migrate.js +111 -0
  116. package/dist/src/storage/cli.js +4 -279
  117. package/dist/src/storage/migration-adapter.js +79 -0
  118. package/dist/src/storage/migration-copy.js +108 -0
  119. package/dist/src/storage/migration-protocol.js +14 -197
  120. package/dist/src/storage/migration-records.js +186 -0
  121. package/dist/src/storage/migration-types.js +12 -0
  122. package/dist/src/tracker/forge-http.js +2 -1
  123. package/dist/src/tracker/issue-pages.js +110 -0
  124. package/package.json +1 -1
  125. package/schemas/mission-v1.schema.json +335 -0
  126. package/schemas/models/model-catalog.v1.schema.json +19 -1
  127. package/schemas/models/model-config-compatibility.v1.schema.json +3 -2
  128. package/schemas/models/model-policy.v1.schema.json +1 -1
  129. package/tools/agents/providers/provider-paths-staging.mjs +35 -0
  130. package/tools/agents/providers/rule-binding-targets.mjs +146 -0
  131. package/tools/agents/providers/rule-delivery.mjs +7 -126
  132. package/vscode-extension/schemas/aiwg.config.v1.json +177 -0
@@ -1,6 +1,6 @@
1
+ import { GATE_RANK } from './lattice.js';
1
2
  import { validateGateDocument } from './schema.js';
2
3
  import { splitPackId } from './registry.js';
3
- const OUTCOME_RANK = { PROMOTE: 0, HOLD: 1, ROLLBACK: 2 };
4
4
  const OUTCOMES = ['PROMOTE', 'HOLD', 'ROLLBACK'];
5
5
  const VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
6
6
  const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
@@ -22,7 +22,7 @@ export const INTEGRITY_CEILING_FLOOR_PACK_VERSION = '1.0.0';
22
22
  export function projectCeilingSatisfied(declared, configured) {
23
23
  if (configured === undefined)
24
24
  return true;
25
- return OUTCOME_RANK[declared ?? 'PROMOTE'] >= OUTCOME_RANK[configured];
25
+ return GATE_RANK[declared ?? 'PROMOTE'] >= GATE_RANK[configured];
26
26
  }
27
27
  /**
28
28
  * Resolves the effective project floors: the configured entries, verbatim.
@@ -64,7 +64,7 @@ export function assertCeilingDefaults(ceilings) {
64
64
  continue;
65
65
  if (!starValid)
66
66
  throw new Error(`gates.ceilings: per-study ceilings require the project-wide '*' default ceiling`);
67
- if (OUTCOME_RANK[ceiling] < OUTCOME_RANK[star]) {
67
+ if (GATE_RANK[ceiling] < GATE_RANK[star]) {
68
68
  throw new Error(`gates.ceilings.${study}: must tighten the project default ceiling ${String(star)}, not loosen it`);
69
69
  }
70
70
  }
@@ -124,7 +124,7 @@ export function validateGatesConfig(gates) {
124
124
  for (const [study, ceiling] of Object.entries(gates.ceilings)) {
125
125
  if (study !== '*' && typeof ceiling === 'string'
126
126
  && OUTCOMES.includes(ceiling)
127
- && OUTCOME_RANK[ceiling] < OUTCOME_RANK[star]) {
127
+ && GATE_RANK[ceiling] < GATE_RANK[star]) {
128
128
  errors.push(`gates.ceilings.${study}: must tighten the project default ceiling ${star}, not loosen it`);
129
129
  }
130
130
  }
@@ -1,17 +1,17 @@
1
1
  /**
2
2
  * Gates capability phase 1 core (epic #2824): declarative gate packs, a pure deterministic
3
- * evaluator and digest-bound reports. Experimental and default-off: nothing in the
4
- * decision runtime imports this module, so existing behavior is unchanged.
3
+ * evaluator and digest-bound reports. Decision consumers share its outcome lattice.
5
4
  */
6
5
  export { GATES_API_VERSION, qualifyGateParameter } from './types.js';
7
6
  export { GateSchemaError, validateGateDocument } from './schema.js';
8
7
  export { GateRegistryError, GATE_NAMESPACES, splitPackId, validateResolvedPack, resolveThresholdDefault, assertGateTightens, applyGateExtends, composeGatePack, resolveGateBinding, expandFloorPacks, enforceProjectFloors, authoredGatePacksOf, gateProvidersOf, GateRegistry, } from './registry.js';
9
8
  export { INTEGRITY_CEILING_FLOOR_PACK_ID, INTEGRITY_CEILING_FLOOR_PACK_VERSION, assertCeilingDefaults, projectCeilingSatisfied, resolveProjectFloors, validateGatesConfig, } from './floors.js';
10
9
  export { GateEvaluationError, maxOutcome, gateStatusOutcome, sealUpstream, sealGateHoldout, evaluateGates, } from './evaluate.js';
11
- export { validateGateReport } from './report.js';
10
+ export { assertGateReportDecision, validateGateReport } from './report.js';
12
11
  export { createCoreProviderRegistry, evidenceProvider, pairedProvider, proportionProvider, scalarProvider, screeningProvider } from './providers/index.js';
13
12
  export { MetricProviderRegistry } from './providers/index.js';
14
13
  export { GATE_PACKS_DIR, GATE_PACK_MAX_BYTES, isGatePackFile, listGatePackFiles, readBundleGatePacksDeclaration, loadGatePackFile, originForPackDir, registerBundleGatePacks, resolveGatePacksDir, discoverShippedGatePacks, } from './discovery.js';
15
14
  export { buildGatesRegistry, validateGateFile, loadHoldoutFile, loadUpstreamFile, evaluateGatesFromFiles, installedAiwgRoot, listGatePacks, loadProjectFloorsForEvaluate, showGatePack, packsReferencedByRule, } from './driver.js';
16
15
  export { GateProviderError, computeProviderCodeDigest, deriveIsolatedSeed, loadGateBundleProviders, invokeBundleProvider, providerBindingPin, runIsolatedProvider, runIsolatedProviderSync, sealProviderSection, snapshotProviderDir, isLoaderIssuedProvider, isolationLaunchPlan, isolationSandboxStatus, parseIsolatedFrame, probeIsolationSandbox, isBundleProvidersEnabled, GATE_PROVIDER_MAX_FILE_BYTES, GATE_PROVIDER_MAX_FILES, GATE_PROVIDER_MAX_TOTAL_BYTES, GATE_PROVIDER_DEFAULT_TIMEOUT_MS, GATE_PROVIDER_MAX_TIMEOUT_MS, GATE_PROVIDER_DEFAULT_MAX_RECORDS, GATE_PROVIDER_MAX_OUTPUT_BYTES, } from './providers/loader.js';
16
+ export { GATE_RANK, ceilingDecision, assertDecisionCeiling, GateDecisionUpgradeError } from './lattice.js';
17
17
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,26 @@
1
+ export const GATE_RANK = Object.freeze({ PROMOTE: 0, HOLD: 1, ROLLBACK: 2 });
2
+ /** The strictest outcome wins; an empty set has no restriction. */
3
+ export function maxOutcome(...outcomes) {
4
+ return outcomes.reduce((worst, outcome) => GATE_RANK[outcome] > GATE_RANK[worst] ? outcome : worst, 'PROMOTE');
5
+ }
6
+ export function ceilingDecision(decision, ...ceilings) {
7
+ return maxOutcome(decision, ...ceilings);
8
+ }
9
+ export class GateDecisionUpgradeError extends Error {
10
+ code = 'report-upgrades-component';
11
+ decision;
12
+ component;
13
+ constructor(decision, component) {
14
+ super(`Report decision differs from components: cannot upgrade ${component} to ${decision} (report-upgrades-component)`);
15
+ this.name = 'GateDecisionUpgradeError';
16
+ this.decision = decision;
17
+ this.component = component;
18
+ }
19
+ }
20
+ /** Validate a carried verdict independently of its digest or reproducibility checks. */
21
+ export function assertDecisionCeiling(decision, ...components) {
22
+ const component = maxOutcome(...components);
23
+ if (GATE_RANK[decision] < GATE_RANK[component])
24
+ throw new GateDecisionUpgradeError(decision, component);
25
+ }
26
+ //# sourceMappingURL=lattice.js.map
@@ -1,3 +1,4 @@
1
+ import { ceilingDecision } from './lattice.js';
1
2
  import { canonicalJson } from '../security/artifact-trust.js';
2
3
  import { artifactDigest } from '../decision/validate.js';
3
4
  import { validateGateDocument } from './schema.js';
@@ -428,7 +429,7 @@ export function assertGateTightens(parent, child, params) {
428
429
  else if (minimumAfter < minimumBefore) {
429
430
  fail(`gate ${child.id} may only raise minimumN`);
430
431
  }
431
- if (parent.onFail === 'ROLLBACK' && child.onFail !== 'ROLLBACK') {
432
+ if (ceilingDecision(child.onFail, parent.onFail) !== child.onFail) {
432
433
  fail(`gate ${child.id} may not demote onFail ROLLBACK -> HOLD`);
433
434
  }
434
435
  // Insufficient evidence always holds: ROLLBACK needs an observed blocking
@@ -1,6 +1,7 @@
1
+ import { assertDecisionCeiling, GateDecisionUpgradeError } from './lattice.js';
1
2
  import { canonicalJson } from '../security/artifact-trust.js';
2
3
  import { artifactDigest } from '../decision/validate.js';
3
- import { evaluateGates, maxOutcome } from './evaluate.js';
4
+ import { evaluateGates } from './evaluate.js';
4
5
  import { validateGateDocument } from './schema.js';
5
6
  /**
6
7
  * Validates a gate report against its trusted inputs. The report is re-derived
@@ -85,12 +86,14 @@ export function validateGateReport(report, input) {
85
86
  }
86
87
  if (expected.decision !== parsed.decision)
87
88
  reasons.push('report-decision-mismatch');
88
- const components = [
89
- ...parsed.gateEvidence.map(entry => entry.outcome),
90
- parsed.ceilings.packOutcome, parsed.ceilings.upstreamCeiling, parsed.ceilings.bindingCeiling,
91
- ];
92
- if (maxOutcome(parsed.decision, maxOutcome(...components)) !== parsed.decision) {
93
- reasons.push('report-upgrades-component');
89
+ try {
90
+ assertGateReportDecision(parsed);
91
+ assertDecisionCeiling(parsed.decision, input.upstream?.metadata.release_gate.decision ?? 'PROMOTE', input.binding.spec.ceiling ?? 'PROMOTE');
92
+ }
93
+ catch (error) {
94
+ if (!(error instanceof GateDecisionUpgradeError))
95
+ throw error;
96
+ return { valid: false, reasons: [...reasons, error.code], error };
94
97
  }
95
98
  return { valid: reasons.length === 0, reasons };
96
99
  }
@@ -99,4 +102,13 @@ function stripDigest(report) {
99
102
  void _dropped;
100
103
  return fields;
101
104
  }
105
+ /** Validate carried component ceilings even when no trusted replay context is available. */
106
+ export function assertGateReportDecision(report) {
107
+ const components = [
108
+ ...report.gateEvidence.map(entry => entry.outcome),
109
+ report.ceilings.packOutcome, report.ceilings.upstreamCeiling, report.ceilings.bindingCeiling,
110
+ report.upstream?.decision ?? 'PROMOTE',
111
+ ];
112
+ assertDecisionCeiling(report.decision, ...components);
113
+ }
102
114
  //# sourceMappingURL=report.js.map
@@ -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