@haystackeditor/cli 0.15.26 → 0.15.27
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 +39 -3
- package/dist/assets/hooks/package-lock.json +598 -0
- package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +2051 -0
- package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +4 -0
- package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +3411 -0
- package/dist/assets/telemetry/browser-runtime.js +861 -0
- package/dist/assets/telemetry/runtime.cjs +1509 -0
- package/dist/commands/cloud-verifier-behaviors.d.ts +27 -0
- package/dist/commands/cloud-verifier-behaviors.js +218 -0
- package/dist/commands/cloud-verifier-data-store-census.d.ts +47 -0
- package/dist/commands/cloud-verifier-data-store-census.js +539 -0
- package/dist/commands/cloud-verifier-data-store-drift.d.ts +42 -0
- package/dist/commands/cloud-verifier-data-store-drift.js +158 -0
- package/dist/commands/cloud-verifier-identity-census.d.ts +88 -0
- package/dist/commands/cloud-verifier-identity-census.js +4057 -0
- package/dist/commands/cloud-verifier-materialization.d.ts +16 -0
- package/dist/commands/cloud-verifier-materialization.js +704 -0
- package/dist/commands/cloud-verifier-pascal-selector-census.d.ts +29 -0
- package/dist/commands/cloud-verifier-pascal-selector-census.js +1382 -0
- package/dist/commands/cloud-verifier-python-manifest-selector-census.d.ts +27 -0
- package/dist/commands/cloud-verifier-python-manifest-selector-census.js +2015 -0
- package/dist/commands/cloud-verifier-specialized-operational-census.d.ts +51 -0
- package/dist/commands/cloud-verifier-specialized-operational-census.js +11432 -0
- package/dist/commands/cloud-verifier-universe.d.ts +31 -0
- package/dist/commands/cloud-verifier-universe.js +10178 -0
- package/dist/commands/design-verify.d.ts +23 -0
- package/dist/commands/design-verify.js +142 -0
- package/dist/commands/hooks.js +2 -2
- package/dist/commands/prepare-universe-review.d.ts +115 -0
- package/dist/commands/prepare-universe-review.js +1092 -0
- package/dist/commands/production-source-deny-policy.d.ts +15 -0
- package/dist/commands/production-source-deny-policy.js +100 -0
- package/dist/commands/scaffold-provisional-universe.d.ts +468 -0
- package/dist/commands/scaffold-provisional-universe.js +808 -0
- package/dist/commands/schema-cmd.js +32 -0
- package/dist/commands/skills.d.ts +2 -2
- package/dist/commands/skills.js +415 -47
- package/dist/commands/telemetry.d.ts +53 -0
- package/dist/commands/telemetry.js +832 -0
- package/dist/index.js +134 -12
- package/dist/triage/runner.js +7 -6
- package/dist/utils/design-verifier-api.d.ts +129 -0
- package/dist/utils/design-verifier-api.js +175 -0
- package/dist/utils/design-verifier-result.d.ts +52 -0
- package/dist/utils/design-verifier-result.js +299 -0
- package/dist/utils/haystack-api.d.ts +2 -0
- package/dist/utils/haystack-api.js +10 -3
- package/dist/utils/hooks.js +2 -1
- package/package.json +7 -2
package/dist/index.js
CHANGED
|
@@ -30,11 +30,18 @@ import { initCommand } from './commands/init.js';
|
|
|
30
30
|
import { authListCommand, authUseCommand, loginCommand, logoutCommand, whoamiCommand, } from './commands/login.js';
|
|
31
31
|
import { handleAgenticTool, handleAutoMerge, handleAutoFix, handleWaitForReviewers, isAutoMergeEnabled, isAutoFixEnabled } from './commands/config.js';
|
|
32
32
|
import { installSkills, listSkills } from './commands/skills.js';
|
|
33
|
+
import { cloudVerifierUniverseValidateCommand } from './commands/cloud-verifier-universe.js';
|
|
34
|
+
import { cloudVerifierIdentityCensusCommand } from './commands/cloud-verifier-identity-census.js';
|
|
35
|
+
import { cloudVerifierDataStoreDriftCommand } from './commands/cloud-verifier-data-store-drift.js';
|
|
36
|
+
import { cloudVerifierBehaviorsValidateCommand } from './commands/cloud-verifier-behaviors.js';
|
|
37
|
+
import { prepareUniverseReviewCommand } from './commands/prepare-universe-review.js';
|
|
38
|
+
import { scaffoldProvisionalUniverseCommand } from './commands/scaffold-provisional-universe.js';
|
|
33
39
|
import { hooksInstall, hooksStatus, hooksUpdate } from './commands/hooks.js';
|
|
34
40
|
import { submitCommand } from './commands/submit.js';
|
|
35
41
|
import { installSessionHooks, sessionHooksStatus } from './commands/install-session-hooks.js';
|
|
36
42
|
import { listPolicies, addPolicy, removePolicy, initPolicies, addInstruction } from './commands/policy.js';
|
|
37
43
|
import { triageCommand } from './commands/triage.js';
|
|
44
|
+
import { designVerifyCommand } from './commands/design-verify.js';
|
|
38
45
|
import { dismissCommand, markReviewedCommand, undismissCommand } from './commands/dismiss.js';
|
|
39
46
|
import { requestReviewCommand } from './commands/request-review.js';
|
|
40
47
|
import { reviewCommand } from './commands/review.js';
|
|
@@ -43,6 +50,7 @@ import { prReadCommand } from './commands/pr.js';
|
|
|
43
50
|
import { inboxListCommand } from './commands/inbox.js';
|
|
44
51
|
import { askHaystackCommand } from './commands/ask.js';
|
|
45
52
|
import { tracesGetCommand, tracesListCommand } from './commands/traces.js';
|
|
53
|
+
import { telemetryInstrumentCommand } from './commands/telemetry.js';
|
|
46
54
|
import { setupCommand } from './commands/setup.js';
|
|
47
55
|
import { schemaCommand, listSchemas } from './commands/schema-cmd.js';
|
|
48
56
|
import { registerWebhook, listWebhooks, rotateWebhookSecret, setWebhookEnabled, listDeliveries, replayDelivery, } from './commands/webhooks.js';
|
|
@@ -804,6 +812,32 @@ traces
|
|
|
804
812
|
.option('--limit <count>', 'Chunks per page', '20')
|
|
805
813
|
.option('--json', 'Output as JSON')
|
|
806
814
|
.action((ref, checkpoint, options) => runPublicCommand(() => tracesGetCommand(ref, checkpoint, options), options.json));
|
|
815
|
+
const telemetry = program.command('telemetry').description('Add privacy-safe production telemetry without an application SDK');
|
|
816
|
+
telemetry
|
|
817
|
+
.command('instrument <dist>')
|
|
818
|
+
.description('Instrument compiled Node.js output without changing application source')
|
|
819
|
+
.requiredOption('--entry <file>', 'Entry file relative to the compiled output directory')
|
|
820
|
+
.option('--source-root <dir>', 'Source directory whose files correspond exactly to compiled output (default: sibling src/)')
|
|
821
|
+
.option('--include-symbols <file>', 'Experimental JSON allow-list of exact sourcePath + qualifiedName symbols')
|
|
822
|
+
.option('--json', 'Machine-readable instrumentation manifest')
|
|
823
|
+
.addHelpText('after', `
|
|
824
|
+
Run this once after the normal application build. It rewrites only compiled
|
|
825
|
+
JavaScript and copies a dependency-free runtime beside it; application source
|
|
826
|
+
does not import Haystack.
|
|
827
|
+
|
|
828
|
+
At runtime, telemetry stays off unless all three variables are set:
|
|
829
|
+
HAYSTACK_TELEMETRY=1
|
|
830
|
+
HAYSTACK_TELEMETRY_ENDPOINT=https://.../v3/telemetry
|
|
831
|
+
HAYSTACK_TELEMETRY_TOKEN=...
|
|
832
|
+
|
|
833
|
+
Example:
|
|
834
|
+
npm run build && haystack telemetry instrument dist --entry index.js
|
|
835
|
+
`)
|
|
836
|
+
.action((dist, options) => runPublicCommand(() => telemetryInstrumentCommand(dist, {
|
|
837
|
+
...options,
|
|
838
|
+
includeSymbolsFile: options.includeSymbols,
|
|
839
|
+
includeSymbols: undefined,
|
|
840
|
+
}), options.json));
|
|
807
841
|
program
|
|
808
842
|
.command('dismiss <pr>')
|
|
809
843
|
.description('Dismiss analysis findings for a PR')
|
|
@@ -826,6 +860,31 @@ Examples:
|
|
|
826
860
|
haystack dismiss acme/widgets#99 # Dismiss for specific repo
|
|
827
861
|
`)
|
|
828
862
|
.action(dismissCommand);
|
|
863
|
+
program
|
|
864
|
+
.command('design-verify <pr>')
|
|
865
|
+
// "verification" is a banned word in top-level help (public-contract test
|
|
866
|
+
// keeps retired auto-fix/verification wording out of the CLI surface).
|
|
867
|
+
.description('Run a blind design-by-execution review of a PR')
|
|
868
|
+
.option('--json', 'Machine-readable output')
|
|
869
|
+
.option('--no-wait', 'Return after the run is admitted instead of waiting for its verdict')
|
|
870
|
+
.option('--run <run-id>', 'Resume and wait for an existing exact run id')
|
|
871
|
+
.option('--poll-interval <seconds>', 'Status polling interval', '3')
|
|
872
|
+
.option('--timeout <seconds>', 'Maximum time to wait; the server run continues after timeout', '7200')
|
|
873
|
+
.addHelpText('after', `
|
|
874
|
+
Executes the change instead of reading it: infers intent from the diff
|
|
875
|
+
alone, designs pre-registered behavioral test cases, runs them against
|
|
876
|
+
both sides of the PR in isolated sandboxes, and reports a mechanical
|
|
877
|
+
verdict of unexpected behavior changes.
|
|
878
|
+
|
|
879
|
+
The command waits for a behavioral verdict by default. Use --no-wait to
|
|
880
|
+
return the run id immediately, or --run <run-id> to resume waiting.
|
|
881
|
+
|
|
882
|
+
PR identifier formats:
|
|
883
|
+
123 PR number (uses current repo)
|
|
884
|
+
owner/repo#123 Fully qualified
|
|
885
|
+
https://github.com/owner/repo/pull/123 GitHub URL
|
|
886
|
+
`)
|
|
887
|
+
.action(designVerifyCommand);
|
|
829
888
|
program
|
|
830
889
|
.command('mark-reviewed <pr>')
|
|
831
890
|
.description('Mark human review as not needed for a PR')
|
|
@@ -1035,32 +1094,95 @@ Examples:
|
|
|
1035
1094
|
// Skills subcommands
|
|
1036
1095
|
const skills = program
|
|
1037
1096
|
.command('skills')
|
|
1038
|
-
.description('Manage AI skills for
|
|
1097
|
+
.description('Manage AI skills for coding agents');
|
|
1039
1098
|
skills
|
|
1040
1099
|
.command('install')
|
|
1041
|
-
.description('Install Haystack skills
|
|
1100
|
+
.description('Install portable Haystack skills and optional coding-CLI shims')
|
|
1042
1101
|
.option('--cli <name>', 'Target CLI: claude, codex, cursor, or manual')
|
|
1043
1102
|
.addHelpText('after', `
|
|
1044
|
-
This
|
|
1045
|
-
/setup-haystack - AI-assisted project setup
|
|
1103
|
+
This installs portable skills in the Git repository's .agents/skills directory:
|
|
1046
1104
|
/submit - Submit a PR via Haystack
|
|
1105
|
+
/map-your-system - Map how Haystack QA can run this system
|
|
1106
|
+
/map-cloud-verifier-universe - Map a production-derived hermetic universe
|
|
1047
1107
|
Supported CLIs:
|
|
1048
|
-
claude Claude Code
|
|
1049
|
-
codex
|
|
1050
|
-
cursor
|
|
1051
|
-
manual
|
|
1108
|
+
claude Also install Claude Code command shims
|
|
1109
|
+
codex Use portable .agents/skills discovery only
|
|
1110
|
+
cursor Use portable .agents/skills discovery only
|
|
1111
|
+
manual Install portable skills and show their location
|
|
1052
1112
|
|
|
1053
1113
|
Examples:
|
|
1054
|
-
haystack skills install #
|
|
1114
|
+
haystack skills install # Install portable skills only
|
|
1055
1115
|
haystack skills install --cli codex # Install for Codex only
|
|
1056
|
-
haystack skills install --cli
|
|
1116
|
+
haystack skills install --cli claude # Also install Claude command shims
|
|
1057
1117
|
`)
|
|
1058
|
-
.action(
|
|
1118
|
+
.action(async (opts) => {
|
|
1119
|
+
try {
|
|
1120
|
+
await installSkills(opts);
|
|
1121
|
+
}
|
|
1122
|
+
catch (err) {
|
|
1123
|
+
console.error(chalk.red('skills install failed:'), err instanceof Error ? err.message : err);
|
|
1124
|
+
process.exit(1);
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1059
1127
|
skills
|
|
1060
1128
|
.command('list')
|
|
1061
1129
|
.description('List available Haystack skills')
|
|
1062
1130
|
.action(listSkills);
|
|
1063
|
-
|
|
1131
|
+
skills
|
|
1132
|
+
.command('census-universe')
|
|
1133
|
+
.description('Deterministically census Cloud Verifier source identities before mapping')
|
|
1134
|
+
.option('--json', 'Machine-readable result')
|
|
1135
|
+
.option('--root <path>', 'Repository root (default: current Git worktree)')
|
|
1136
|
+
.action((opts) => runPublicCommand(async () => cloudVerifierIdentityCensusCommand(opts), opts.json));
|
|
1137
|
+
skills
|
|
1138
|
+
.command('data-store-drift')
|
|
1139
|
+
.description('Check whether the detected data stores still match the checked-in map (per-PR drift gate)')
|
|
1140
|
+
.option('--json', 'Machine-readable result')
|
|
1141
|
+
.option('--root <path>', 'Repository root (default: current Git worktree)')
|
|
1142
|
+
.option('--baseline <path>', 'Baseline map path (default: .haystack/cloud-verifier/data-stores.json)')
|
|
1143
|
+
.option('--base-ref <ref>', 'Only run when the diff against this ref touches a store-relevant file')
|
|
1144
|
+
.option('--update', 'Write the current detected stores as the new baseline')
|
|
1145
|
+
.action((opts) => runPublicCommand(async () => cloudVerifierDataStoreDriftCommand(opts), opts.json));
|
|
1146
|
+
skills
|
|
1147
|
+
.command('validate-universe')
|
|
1148
|
+
.description('Validate Cloud Verifier universe artifacts and stable-ID references')
|
|
1149
|
+
.option('--json', 'Machine-readable result')
|
|
1150
|
+
.action(async (opts) => {
|
|
1151
|
+
try {
|
|
1152
|
+
await cloudVerifierUniverseValidateCommand(opts);
|
|
1153
|
+
}
|
|
1154
|
+
catch (err) {
|
|
1155
|
+
console.error(chalk.red('validate-universe failed:'), err instanceof Error ? err.message : err);
|
|
1156
|
+
process.exit(1);
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
skills
|
|
1160
|
+
.command('validate-behaviors')
|
|
1161
|
+
.description('Validate customer actions and their exact runtime identities')
|
|
1162
|
+
.option('--json', 'Machine-readable result')
|
|
1163
|
+
.action(async (opts) => {
|
|
1164
|
+
try {
|
|
1165
|
+
await cloudVerifierBehaviorsValidateCommand(opts);
|
|
1166
|
+
}
|
|
1167
|
+
catch (err) {
|
|
1168
|
+
console.error(chalk.red('validate-behaviors failed:'), err instanceof Error ? err.message : err);
|
|
1169
|
+
process.exit(1);
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
skills
|
|
1173
|
+
.command('scaffold-provisional-universe')
|
|
1174
|
+
.description('Create a compact missing-authority receipt from strict JSON facts')
|
|
1175
|
+
.requiredOption('--input <path>', 'Strict provisional-universe JSON input file')
|
|
1176
|
+
.action((opts) => runPublicCommand(async () => scaffoldProvisionalUniverseCommand(opts), true));
|
|
1177
|
+
skills
|
|
1178
|
+
.command('prepare-universe-review')
|
|
1179
|
+
.description('Create a tracked-source snapshot with conventional test paths removed')
|
|
1180
|
+
.option('--json', 'Machine-readable isolation result and snapshot manifest')
|
|
1181
|
+
.option('--exclude-inactive-submodule <path...>', 'Omit exact tracked gitlink paths explicitly known to be inactive')
|
|
1182
|
+
.option('--omit-unscannable-files', 'Omit files whose test content cannot be ruled out, instead of blocking the review. '
|
|
1183
|
+
+ 'The reviewer still never sees a test; the omissions are recorded as uncovered scope')
|
|
1184
|
+
.option('--full-receipt', 'Include complete non-blocking path receipts in JSON output')
|
|
1185
|
+
.action((opts) => runPublicCommand(() => prepareUniverseReviewCommand(opts), opts.json));
|
|
1064
1186
|
const hooks = program
|
|
1065
1187
|
.command('hooks')
|
|
1066
1188
|
.description('Manage git hooks for AI agent quality checks');
|
package/dist/triage/runner.js
CHANGED
|
@@ -101,7 +101,13 @@ export function buildCommand(cli, prompt, maxTurns) {
|
|
|
101
101
|
case 'codex':
|
|
102
102
|
return {
|
|
103
103
|
command: 'codex',
|
|
104
|
-
args: [
|
|
104
|
+
args: [
|
|
105
|
+
'--sandbox', 'workspace-write',
|
|
106
|
+
'--ask-for-approval', 'never',
|
|
107
|
+
'exec',
|
|
108
|
+
'--ephemeral',
|
|
109
|
+
prompt,
|
|
110
|
+
],
|
|
105
111
|
};
|
|
106
112
|
case 'gemini':
|
|
107
113
|
return {
|
|
@@ -119,17 +125,12 @@ function spawnChecker(cli, config, cwd, timeoutMs) {
|
|
|
119
125
|
const { command, args } = buildCommand(cli, config.prompt, config.maxTurns);
|
|
120
126
|
let stdout = '';
|
|
121
127
|
let stderr = '';
|
|
122
|
-
const codexHome = join(cwd, '.haystack', 'triage', 'codex-home');
|
|
123
|
-
if (cli === 'codex') {
|
|
124
|
-
mkdirSync(codexHome, { recursive: true });
|
|
125
|
-
}
|
|
126
128
|
const proc = spawn(command, args, {
|
|
127
129
|
cwd,
|
|
128
130
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
129
131
|
env: {
|
|
130
132
|
...process.env,
|
|
131
133
|
CLAUDECODE: undefined,
|
|
132
|
-
CODEX_HOME: cli === 'codex' ? codexHome : process.env.CODEX_HOME,
|
|
133
134
|
},
|
|
134
135
|
});
|
|
135
136
|
proc.stdout?.on('data', (data) => {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client for the design-verifier trigger route on the auth worker.
|
|
3
|
+
*
|
|
4
|
+
* The worker owns everything sensitive: it authenticates the CLI token,
|
|
5
|
+
* enforces hsk_live repo scope, resolves the PR's base/head SHAs via the
|
|
6
|
+
* GitHub App install token, and forwards to the AWS design-verifier API with
|
|
7
|
+
* the server-side shared secret. This client just POSTs and interprets the
|
|
8
|
+
* response.
|
|
9
|
+
*/
|
|
10
|
+
export type DesignVerifierTriggerResult = {
|
|
11
|
+
status: 'started' | 'queued' | 'duplicate';
|
|
12
|
+
runId: string;
|
|
13
|
+
} | {
|
|
14
|
+
status: 'denied';
|
|
15
|
+
message: string;
|
|
16
|
+
} | {
|
|
17
|
+
status: 'rate_limited';
|
|
18
|
+
message: string;
|
|
19
|
+
} | {
|
|
20
|
+
status: 'unavailable';
|
|
21
|
+
message: string;
|
|
22
|
+
} | {
|
|
23
|
+
status: 'error';
|
|
24
|
+
message: string;
|
|
25
|
+
};
|
|
26
|
+
export interface DesignVerifierCommandSpec {
|
|
27
|
+
cwd: string;
|
|
28
|
+
argv: string[];
|
|
29
|
+
env: Record<string, string>;
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
}
|
|
32
|
+
export interface DesignVerifierExecutionDiagnostic {
|
|
33
|
+
diagnosticId: string;
|
|
34
|
+
kind: string;
|
|
35
|
+
message: string;
|
|
36
|
+
type?: string;
|
|
37
|
+
code?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface DesignVerifierExecutionSummary {
|
|
40
|
+
side: 'base1' | 'base2' | 'head';
|
|
41
|
+
status: 'completed' | 'failed' | 'missing';
|
|
42
|
+
commandExitCode: number | null;
|
|
43
|
+
durationMs: number | null;
|
|
44
|
+
scenarioError: string | null;
|
|
45
|
+
diagnostics: DesignVerifierExecutionDiagnostic[];
|
|
46
|
+
diagnosticsOmitted: number;
|
|
47
|
+
outputExcerpt: Array<{
|
|
48
|
+
stream: 'stdout' | 'stderr' | 'system' | 'output';
|
|
49
|
+
text: string;
|
|
50
|
+
}>;
|
|
51
|
+
outputLinesOmitted: number;
|
|
52
|
+
traceArtifact: string;
|
|
53
|
+
outputArtifact: string;
|
|
54
|
+
}
|
|
55
|
+
export interface DesignVerifierStageTiming {
|
|
56
|
+
stage: 'prepare' | 'bake' | 'blast-radius' | 'prefetch' | 'case-design' | 'verdict';
|
|
57
|
+
status: string;
|
|
58
|
+
durationMs: number | null;
|
|
59
|
+
completedAt: string | null;
|
|
60
|
+
source: 'phase-artifact' | 'phase-artifact-clock' | 'cache-hit' | 'missing';
|
|
61
|
+
breakdownMs: Record<string, number>;
|
|
62
|
+
pollCount?: number;
|
|
63
|
+
}
|
|
64
|
+
export interface DesignVerifierTimingSummary {
|
|
65
|
+
admissionMs: number | null;
|
|
66
|
+
pipelineDurationMs: number | null;
|
|
67
|
+
recordedStageMs: number;
|
|
68
|
+
unattributedMs: number | null;
|
|
69
|
+
stages: DesignVerifierStageTiming[];
|
|
70
|
+
}
|
|
71
|
+
export interface DesignVerifierStatusResult {
|
|
72
|
+
version: 'dv-cli-status-v1';
|
|
73
|
+
runId: string;
|
|
74
|
+
owner: string;
|
|
75
|
+
repo: string;
|
|
76
|
+
pr: number;
|
|
77
|
+
baseSha: string | null;
|
|
78
|
+
headSha: string | null;
|
|
79
|
+
status: string;
|
|
80
|
+
stage: string | null;
|
|
81
|
+
createdAt: string | null;
|
|
82
|
+
startedAt: string | null;
|
|
83
|
+
finishedAt: string | null;
|
|
84
|
+
durationMs: number | null;
|
|
85
|
+
error: string | null;
|
|
86
|
+
result?: {
|
|
87
|
+
verdict: Record<string, any>;
|
|
88
|
+
cases: Array<Record<string, any>>;
|
|
89
|
+
scenarios: Array<Record<string, any> & {
|
|
90
|
+
executions?: DesignVerifierExecutionSummary[];
|
|
91
|
+
}>;
|
|
92
|
+
changedFiles: string[];
|
|
93
|
+
timings?: DesignVerifierTimingSummary;
|
|
94
|
+
probes: Array<{
|
|
95
|
+
caseId: string;
|
|
96
|
+
title: string;
|
|
97
|
+
files: string[];
|
|
98
|
+
validate: DesignVerifierCommandSpec | null;
|
|
99
|
+
run: DesignVerifierCommandSpec | null;
|
|
100
|
+
}>;
|
|
101
|
+
replay: {
|
|
102
|
+
artifactPrefix: string;
|
|
103
|
+
keys: Record<string, string>;
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export type DesignVerifierStatusFetchResult = {
|
|
108
|
+
status: 'ok';
|
|
109
|
+
run: DesignVerifierStatusResult;
|
|
110
|
+
} | {
|
|
111
|
+
status: 'not_found' | 'denied' | 'unavailable' | 'error';
|
|
112
|
+
message: string;
|
|
113
|
+
};
|
|
114
|
+
export declare function triggerDesignVerifier(owner: string, repo: string, prNumber: number, token: string): Promise<DesignVerifierTriggerResult>;
|
|
115
|
+
export declare function getDesignVerifierStatus(owner: string, repo: string, prNumber: number, runId: string, token: string): Promise<DesignVerifierStatusFetchResult>;
|
|
116
|
+
export declare function waitForDesignVerifier(owner: string, repo: string, prNumber: number, runId: string, token: string, options?: {
|
|
117
|
+
intervalMs?: number;
|
|
118
|
+
timeoutMs?: number;
|
|
119
|
+
onProgress?: (run: DesignVerifierStatusResult) => void;
|
|
120
|
+
sleep?: (ms: number) => Promise<void>;
|
|
121
|
+
}): Promise<{
|
|
122
|
+
status: 'terminal';
|
|
123
|
+
run: DesignVerifierStatusResult;
|
|
124
|
+
} | {
|
|
125
|
+
status: 'timeout';
|
|
126
|
+
} | {
|
|
127
|
+
status: 'error';
|
|
128
|
+
message: string;
|
|
129
|
+
}>;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client for the design-verifier trigger route on the auth worker.
|
|
3
|
+
*
|
|
4
|
+
* The worker owns everything sensitive: it authenticates the CLI token,
|
|
5
|
+
* enforces hsk_live repo scope, resolves the PR's base/head SHAs via the
|
|
6
|
+
* GitHub App install token, and forwards to the AWS design-verifier API with
|
|
7
|
+
* the server-side shared secret. This client just POSTs and interprets the
|
|
8
|
+
* response.
|
|
9
|
+
*/
|
|
10
|
+
import { haystackApiUrl } from './haystack-api.js';
|
|
11
|
+
function isDuration(value) {
|
|
12
|
+
return value === null || (typeof value === 'number' && Number.isFinite(value) && value >= 0);
|
|
13
|
+
}
|
|
14
|
+
function isTimestamp(value) {
|
|
15
|
+
return value === null || (typeof value === 'string' && Number.isFinite(Date.parse(value)));
|
|
16
|
+
}
|
|
17
|
+
function isTimingSummary(value) {
|
|
18
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
19
|
+
return false;
|
|
20
|
+
const timing = value;
|
|
21
|
+
if (!isDuration(timing.admissionMs) || !isDuration(timing.pipelineDurationMs) ||
|
|
22
|
+
typeof timing.recordedStageMs !== 'number' || !isDuration(timing.recordedStageMs) ||
|
|
23
|
+
!isDuration(timing.unattributedMs) ||
|
|
24
|
+
!Array.isArray(timing.stages))
|
|
25
|
+
return false;
|
|
26
|
+
const expectedStages = ['prepare', 'bake', 'blast-radius', 'prefetch', 'case-design', 'verdict'];
|
|
27
|
+
return timing.stages.length === expectedStages.length && timing.stages.every((stage, index) => stage && typeof stage === 'object' && stage.stage === expectedStages[index] &&
|
|
28
|
+
typeof stage.status === 'string' && isDuration(stage.durationMs) && isTimestamp(stage.completedAt) &&
|
|
29
|
+
['phase-artifact', 'phase-artifact-clock', 'cache-hit', 'missing'].includes(stage.source) &&
|
|
30
|
+
stage.breakdownMs && typeof stage.breakdownMs === 'object' && !Array.isArray(stage.breakdownMs) &&
|
|
31
|
+
Object.values(stage.breakdownMs).every((duration) => typeof duration === 'number' && isDuration(duration)));
|
|
32
|
+
}
|
|
33
|
+
function isDesignVerifierStatusResult(value, expected) {
|
|
34
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
35
|
+
return false;
|
|
36
|
+
const run = value;
|
|
37
|
+
if (run.version !== 'dv-cli-status-v1' || run.runId !== expected.runId ||
|
|
38
|
+
run.owner !== expected.owner || run.repo !== expected.repo || run.pr !== expected.pr ||
|
|
39
|
+
typeof run.status !== 'string')
|
|
40
|
+
return false;
|
|
41
|
+
if (run.status !== 'succeeded')
|
|
42
|
+
return true;
|
|
43
|
+
return !!run.result && typeof run.result === 'object' &&
|
|
44
|
+
run.result.verdict && typeof run.result.verdict === 'object' &&
|
|
45
|
+
Array.isArray(run.result.cases) && Array.isArray(run.result.scenarios) &&
|
|
46
|
+
Array.isArray(run.result.changedFiles) && Array.isArray(run.result.probes) &&
|
|
47
|
+
(run.result.timings === undefined || isTimingSummary(run.result.timings)) &&
|
|
48
|
+
run.result.replay && typeof run.result.replay === 'object';
|
|
49
|
+
}
|
|
50
|
+
function apiHeaders(token) {
|
|
51
|
+
return {
|
|
52
|
+
'Cache-Control': 'no-cache',
|
|
53
|
+
'User-Agent': 'Haystack-CLI',
|
|
54
|
+
Authorization: `Bearer ${token}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export async function triggerDesignVerifier(owner, repo, prNumber, token) {
|
|
58
|
+
const url = haystackApiUrl(`/api/design-verifier/trigger/${owner}/${repo}/${prNumber}`);
|
|
59
|
+
let response;
|
|
60
|
+
try {
|
|
61
|
+
response = await fetch(url, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { ...apiHeaders(token), 'Content-Type': 'application/json' },
|
|
64
|
+
body: '{}',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
return { status: 'error', message: err.message };
|
|
69
|
+
}
|
|
70
|
+
let data = {};
|
|
71
|
+
let parseFailed = false;
|
|
72
|
+
try {
|
|
73
|
+
data = await response.json();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Recorded, not swallowed: non-OK statuses are classified below; a 2xx
|
|
77
|
+
// with an unparseable body becomes an explicit error rather than a
|
|
78
|
+
// silently-empty success.
|
|
79
|
+
parseFailed = true;
|
|
80
|
+
}
|
|
81
|
+
if (parseFailed && response.ok) {
|
|
82
|
+
return { status: 'error', message: `Trigger endpoint returned invalid JSON (HTTP ${response.status})` };
|
|
83
|
+
}
|
|
84
|
+
if (response.ok) {
|
|
85
|
+
const outcome = data.outcome === 'queued' || data.outcome === 'duplicate' ? data.outcome : 'started';
|
|
86
|
+
if (!data.runId) {
|
|
87
|
+
return { status: 'error', message: `Trigger succeeded but no runId was returned (HTTP ${response.status})` };
|
|
88
|
+
}
|
|
89
|
+
return { status: outcome, runId: data.runId };
|
|
90
|
+
}
|
|
91
|
+
const message = data.message || data.error || `HTTP ${response.status}`;
|
|
92
|
+
// API Gateway answers 403 "Missing Authentication Token" for routes that
|
|
93
|
+
// do not exist — which is exactly what the flag-gated backend looks like
|
|
94
|
+
// before enable_design_verifier is on. The worker's own denials carry
|
|
95
|
+
// specific error codes/messages, so this signature is unambiguous.
|
|
96
|
+
if (response.status === 403 && data.message === 'Missing Authentication Token') {
|
|
97
|
+
return { status: 'unavailable', message: 'Design verifier is not enabled in this environment yet.' };
|
|
98
|
+
}
|
|
99
|
+
if (response.status === 401 || response.status === 403)
|
|
100
|
+
return { status: 'denied', message };
|
|
101
|
+
if (response.status === 429)
|
|
102
|
+
return { status: 'rate_limited', message };
|
|
103
|
+
if (response.status === 404) {
|
|
104
|
+
// Two distinct 404s share this status: the worker's own pr_not_found
|
|
105
|
+
// (carries an error code and a specific message) versus the AWS gateway
|
|
106
|
+
// 404 for the flag-gated /v3/design-verifier route not being enabled
|
|
107
|
+
// yet. Only the latter means "feature unavailable".
|
|
108
|
+
if (data.error === 'pr_not_found') {
|
|
109
|
+
return { status: 'error', message };
|
|
110
|
+
}
|
|
111
|
+
return { status: 'unavailable', message: 'Design verifier is not enabled in this environment yet.' };
|
|
112
|
+
}
|
|
113
|
+
return { status: 'error', message };
|
|
114
|
+
}
|
|
115
|
+
export async function getDesignVerifierStatus(owner, repo, prNumber, runId, token) {
|
|
116
|
+
const url = haystackApiUrl(`/api/design-verifier/status/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${prNumber}/${encodeURIComponent(runId)}`);
|
|
117
|
+
let response;
|
|
118
|
+
try {
|
|
119
|
+
response = await fetch(url, { method: 'GET', headers: apiHeaders(token) });
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
return { status: 'error', message: err.message };
|
|
123
|
+
}
|
|
124
|
+
let data = {};
|
|
125
|
+
try {
|
|
126
|
+
data = await response.json();
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return { status: 'error', message: `Status endpoint returned invalid JSON (HTTP ${response.status})` };
|
|
130
|
+
}
|
|
131
|
+
if (response.ok) {
|
|
132
|
+
if (!isDesignVerifierStatusResult(data, { owner, repo, pr: prNumber, runId })) {
|
|
133
|
+
return { status: 'error', message: `Status endpoint returned an invalid or mismatched result (HTTP ${response.status})` };
|
|
134
|
+
}
|
|
135
|
+
return { status: 'ok', run: data };
|
|
136
|
+
}
|
|
137
|
+
const message = data.message || data.error || `HTTP ${response.status}`;
|
|
138
|
+
if (response.status === 401 || response.status === 403)
|
|
139
|
+
return { status: 'denied', message };
|
|
140
|
+
if (response.status === 404)
|
|
141
|
+
return { status: 'not_found', message };
|
|
142
|
+
if (response.status === 502 || response.status === 503)
|
|
143
|
+
return { status: 'unavailable', message };
|
|
144
|
+
return { status: 'error', message };
|
|
145
|
+
}
|
|
146
|
+
export async function waitForDesignVerifier(owner, repo, prNumber, runId, token, options = {}) {
|
|
147
|
+
const intervalMs = options.intervalMs ?? 3000;
|
|
148
|
+
const timeoutMs = options.timeoutMs ?? 2 * 60 * 60 * 1000;
|
|
149
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
150
|
+
const deadline = Date.now() + timeoutMs;
|
|
151
|
+
let consecutiveErrors = 0;
|
|
152
|
+
let lastStage;
|
|
153
|
+
for (;;) {
|
|
154
|
+
const loaded = await getDesignVerifierStatus(owner, repo, prNumber, runId, token);
|
|
155
|
+
if (loaded.status !== 'ok') {
|
|
156
|
+
consecutiveErrors += 1;
|
|
157
|
+
if (loaded.status === 'denied' || loaded.status === 'not_found' || consecutiveErrors >= 3) {
|
|
158
|
+
return { status: 'error', message: loaded.message };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
consecutiveErrors = 0;
|
|
163
|
+
if (loaded.run.stage !== lastStage) {
|
|
164
|
+
lastStage = loaded.run.stage;
|
|
165
|
+
options.onProgress?.(loaded.run);
|
|
166
|
+
}
|
|
167
|
+
if (['succeeded', 'failed', 'failed-stale'].includes(loaded.run.status)) {
|
|
168
|
+
return { status: 'terminal', run: loaded.run };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (Date.now() >= deadline)
|
|
172
|
+
return { status: 'timeout' };
|
|
173
|
+
await sleep(intervalMs);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { DesignVerifierCommandSpec, DesignVerifierExecutionSummary, DesignVerifierStatusResult, DesignVerifierTimingSummary } from './design-verifier-api.js';
|
|
2
|
+
export type DesignVerifierAgentVerdict = 'fix_confirmed' | 'fix_needed' | 'no_unexpected_change' | 'inconclusive';
|
|
3
|
+
export interface DesignVerifierAgentCase {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
story: string;
|
|
7
|
+
expectedHead: string;
|
|
8
|
+
verdict: 'confirmed' | 'finding' | 'clean' | 'inconclusive';
|
|
9
|
+
findingKind: string | null;
|
|
10
|
+
baseStable: boolean | null;
|
|
11
|
+
observations: Array<{
|
|
12
|
+
path: string;
|
|
13
|
+
base: unknown;
|
|
14
|
+
baseRepeat: unknown;
|
|
15
|
+
head: unknown;
|
|
16
|
+
}>;
|
|
17
|
+
generatedFiles: string[];
|
|
18
|
+
executedCommand: DesignVerifierCommandSpec | null;
|
|
19
|
+
executions: DesignVerifierExecutionSummary[];
|
|
20
|
+
infrastructure: {
|
|
21
|
+
verified: boolean;
|
|
22
|
+
recipeId: string | null;
|
|
23
|
+
freshInstances: number;
|
|
24
|
+
baseRepeat: string | null;
|
|
25
|
+
errors: string[];
|
|
26
|
+
} | null;
|
|
27
|
+
}
|
|
28
|
+
export interface DesignVerifierAgentReport {
|
|
29
|
+
ok: boolean;
|
|
30
|
+
status: 'complete' | 'failed';
|
|
31
|
+
verdict: DesignVerifierAgentVerdict;
|
|
32
|
+
headline: string;
|
|
33
|
+
explanation: string;
|
|
34
|
+
runId: string;
|
|
35
|
+
repository: string;
|
|
36
|
+
pullRequest: number;
|
|
37
|
+
revisions: {
|
|
38
|
+
base: string | null;
|
|
39
|
+
head: string | null;
|
|
40
|
+
};
|
|
41
|
+
durationMs: number | null;
|
|
42
|
+
timings: DesignVerifierTimingSummary | null;
|
|
43
|
+
changedFiles: string[];
|
|
44
|
+
cases: DesignVerifierAgentCase[];
|
|
45
|
+
inspectCommand: string;
|
|
46
|
+
replay: Record<string, unknown> | null;
|
|
47
|
+
error: string | null;
|
|
48
|
+
exitCode: 0 | 1 | 2;
|
|
49
|
+
}
|
|
50
|
+
export declare function buildDesignVerifierAgentReport(run: DesignVerifierStatusResult): DesignVerifierAgentReport;
|
|
51
|
+
export declare function formatDesignVerifierAgentReport(report: DesignVerifierAgentReport): string;
|
|
52
|
+
export declare function designVerifierStageLabel(stage: string | null): string;
|